Compare commits
57 Commits
8493f0df03
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cf65ceed1c | |||
| f87cd8a68e | |||
| eb0a4005e1 | |||
| 73de137779 | |||
| aaaac95b53 | |||
| 4a5dc61a02 | |||
| 021bf311c1 | |||
| f1d6a78f8a | |||
| 9e52c95400 | |||
| 0c69d4c347 | |||
| 62fd22cb31 | |||
| d8d4deffd1 | |||
| 65b33609d6 | |||
| 3615902e8d | |||
| f0110df164 | |||
| d7f92f1816 | |||
| a889671bb5 | |||
| aaf887507f | |||
| b934b737e8 | |||
| cf465d28e2 | |||
| e65dcc2d39 | |||
| 0dfb3c63b1 | |||
| 8f8a7ea00a | |||
| b03431b511 | |||
| 7717c9fa14 | |||
| 2fd1b3da21 | |||
| 4373fafd55 | |||
| a548623ea5 | |||
| 6baa6b0d0a | |||
| 483f158424 | |||
| 505f3591ab | |||
| 2c9ba9d6b3 | |||
| 79441a8a87 | |||
| 64dc85bd14 | |||
| c51e6b793a | |||
| e728dcd226 | |||
| 0e6b3b1811 | |||
| 4537faf2c4 | |||
| 3f120417d1 | |||
| 6464e89942 | |||
| 4fbff0761a | |||
| 5e531ffe1e | |||
| bee439cf34 | |||
| 3ea59551db | |||
| e6ddea33a2 | |||
| 3b5f70f1f3 | |||
| eab4137c0a | |||
| 4ec6f98f10 | |||
| 2f811b986c | |||
| 5016458e44 | |||
| a330757946 | |||
| 853c478657 | |||
| cf6d708566 | |||
| a8c0e1af3d | |||
| 00ca81287e | |||
| 66ba6e00f5 | |||
| f3d2951dc3 |
+2
-1
@@ -53,7 +53,8 @@ tmp/
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
deploy/
|
||||
# 注意:deploy/ 不能排除——Dockerfile.moldinsight COPY deploy/requirements-*.txt、
|
||||
# Dockerfile.frontend COPY deploy/nginx/frontend.conf,排除会让干净机器首次构建必挂
|
||||
|
||||
# 其他
|
||||
.trae/
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# 服务配置
|
||||
HOST=0.0.0.0
|
||||
# ================================
|
||||
# 端口配置 - 唯一修改端口的地方
|
||||
# ================================
|
||||
# gemold(moldinsight)API 对外端口
|
||||
MOLDINSIGHT_PORT=10003
|
||||
# inventory API 对外端口
|
||||
INVENTORY_PORT=10004
|
||||
# unified 模式下前端 Nginx 对外端口(浏览器入口)
|
||||
FRONTEND_PORT=10003
|
||||
# unified 模式 backend 是否暴露宿主端口:留空 = 不暴露(仅经前端 /api 反代)
|
||||
BACKEND_PORT=
|
||||
# 应用内部监听端口(通常无需修改;uvicorn 命令硬编码 8000/8001)
|
||||
# PORT 与 HOST 仅在直跑 uvicorn 时生效,compose 容器内 uvicorn 不读这两个 env
|
||||
HOST=0.0.0.0
|
||||
PORT=10003
|
||||
# ================================
|
||||
|
||||
|
||||
|
||||
DEBUG=false
|
||||
|
||||
# 文件处理配置
|
||||
UPLOAD_DIR=./uploads
|
||||
MAX_FILE_SIZE=104857600
|
||||
ALLOWED_EXTENSIONS=.stp,.step,.stp.gz
|
||||
|
||||
# 几何处理配置
|
||||
POINTCLOUD_SAMPLE_COUNT=10000
|
||||
MESH_QUALITY=high
|
||||
PARALLEL_PROCESSING=true
|
||||
|
||||
# 数据库配置——以部署机实测可达 IP 为准(之前 192.168.3.10 → 192.168.0.11),
|
||||
# 改 IP 后必须重启 backend 才生效
|
||||
DB_HOST=192.168.0.11
|
||||
DB_PORT=5432
|
||||
DB_NAME=moldinsight
|
||||
DB_USER=moldinsight
|
||||
DB_PASSWORD=Qqs1996
|
||||
|
||||
# RustFS 对象存储配置 (S3v4 API)
|
||||
RUSTFS_ENDPOINT=http://192.168.0.11:8010
|
||||
RUSTFS_ACCESS_KEY=1RlKXw7v3DAsFr4fLckt
|
||||
RUSTFS_SECRET_KEY=KjWCHXZOh7GAtkLq0eQgNpMSmE6zw8Ddyiou21bB
|
||||
RUSTFS_TIMEOUT=30
|
||||
RUSTFS_PRESIGNED_URL_EXPIRES=3600
|
||||
|
||||
# JWT认证配置
|
||||
SECRET_KEY=vGLxbDGj4I3LnWZYQqRrchlVBSWpM73IZ8fT7ldwpsXpDYB82ghkr7sRkO7D-BiR
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=1440
|
||||
|
||||
# Redis配置(与 DB 同 IP)
|
||||
REDIS_HOST=192.168.0.11
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=Qqs1996
|
||||
REDIS_DB=0
|
||||
|
||||
# 管理员配置
|
||||
ADMIN_USERNAME=cjw
|
||||
ADMIN_PASSWORD=Qqs1996
|
||||
ADMIN_EMAIL=792430652@qq.com
|
||||
ADMIN_FULL_NAME=管理员
|
||||
|
||||
|
||||
|
||||
# LLM 增强分析配置(可选)
|
||||
# 启用后自动生成模具设计评审报告和分模方向推荐
|
||||
# 支持 OpenAI 兼容 API(OpenAI / DeepSeek / vLLM / Ollama 等)
|
||||
LLM_ENABLED=false
|
||||
LLM_API_URL=https://api.deepseek.com/v1
|
||||
LLM_API_KEY=sk-509f968af3e2466bbef8b5949180782c
|
||||
LLM_MODEL=deepseek-v4-flash
|
||||
LLM_TIMEOUT=60
|
||||
LLM_MAX_TOKENS=2000
|
||||
+37
-10
@@ -1,17 +1,25 @@
|
||||
# 服务配置
|
||||
HOST=0.0.0.0
|
||||
|
||||
# ================================
|
||||
# 端口配置 - 唯一修改端口的地方
|
||||
# 端口配置 — DEPLOYMENT.md §1.2 选择 A 为默认(前端独占宿主端口,backend 不暴露)
|
||||
# ================================
|
||||
# 应用端口(容器内端口)
|
||||
PORT=8000
|
||||
# Docker映射到宿主机的端口(docker-compose使用)
|
||||
HOST_PORT=10001
|
||||
# 容器内部端口统一 8000(frontend=8000 / backend=8000 / inventory=8001);
|
||||
# 服务间通过 docker 网络 gemold_network 上的服务名(如 backend:8000)互通。
|
||||
# 宿主机端口由本文件强制配置,compose 无默认值兜底:
|
||||
FRONTEND_PORT=10003 # 浏览器入口(必填,缺则 compose 启动期 fail-fast)
|
||||
MOLDINSIGHT_PORT=10003 # moldinsight-only 独立部署(必填)
|
||||
INVENTORY_PORT=10004 # inventory-only 独立部署(必填)
|
||||
# unified 模式 backend 是否暴露宿主端口:留空 = 不暴露(仅经前端 /api 反代),
|
||||
# 设值(如 10005)= 直接暴露(调试 / 压测用,注意 10003 已被 frontend 占用)
|
||||
BACKEND_PORT=
|
||||
# ================================
|
||||
|
||||
DEBUG=false
|
||||
|
||||
# 日志配置
|
||||
# LOG_FORMAT: json(生产默认,结构化输出)/ text(开发默认,人可读)
|
||||
# LOG_LEVEL: DEBUG / INFO / WARNING / ERROR
|
||||
LOG_FORMAT=json
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# 文件处理配置
|
||||
UPLOAD_DIR=./uploads
|
||||
MAX_FILE_SIZE=104857600
|
||||
@@ -22,25 +30,39 @@ POINTCLOUD_SAMPLE_COUNT=10000
|
||||
MESH_QUALITY=high
|
||||
PARALLEL_PROCESSING=true
|
||||
|
||||
# 数据库配置
|
||||
# 数据库配置(服务器已部署,请填写真实地址)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
|
||||
# 是否在应用启动时自动执行 alembic 迁移(默认 true,保持单机开发体验)。
|
||||
# 多副本/容器编排部署建议设为 false:多个实例同时启动会并发迁移,
|
||||
# 改由部署流程单点执行 alembic CLI 或 python -m shared.database.init_db
|
||||
AUTO_MIGRATE=true
|
||||
DB_NAME=moldinsight
|
||||
DB_USER=moldinsight_user
|
||||
DB_PASSWORD=moldinsight_password
|
||||
|
||||
# RustFS 对象存储配置 (S3v4 API)
|
||||
# RustFS / MinIO 兼容对象存储配置(服务器已部署,请填写真实地址)
|
||||
RUSTFS_ENDPOINT=http://localhost:9000
|
||||
RUSTFS_ACCESS_KEY=your-access-key
|
||||
RUSTFS_SECRET_KEY=your-secret-key
|
||||
RUSTFS_TIMEOUT=30
|
||||
RUSTFS_PRESIGNED_URL_EXPIRES=3600
|
||||
|
||||
# Redis 配置(服务器已部署,请填写真实地址)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# JWT认证配置
|
||||
SECRET_KEY=your-secret-key-change-in-production-min-32-chars
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=1440
|
||||
|
||||
# CORS 白名单(逗号分隔,不设置则默认为 [*],生产环境务必设置)
|
||||
# CORS_ORIGINS=http://localhost:5173,http://localhost:8000,http://localhost:8001,https://your-domain.com
|
||||
|
||||
# 管理员账户配置
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=change-this-to-a-secure-password
|
||||
@@ -61,3 +83,8 @@ LLM_API_KEY=sk-your-api-key
|
||||
LLM_MODEL=gpt-4o-mini
|
||||
LLM_TIMEOUT=60
|
||||
LLM_MAX_TOKENS=2000
|
||||
|
||||
# Celery/OCC 吞吐调优(可选,默认值在 compose 的 ${CELERY_CONCURRENCY:-2};
|
||||
# concurrency 即 OCC 并行分析数,见 docs/topics/performance/OCC_THROUGHPUT.md)
|
||||
# CELERY_CONCURRENCY=2
|
||||
# CELERY_MAX_TASKS_PER_CHILD=50
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# CI 门禁(Gitea Actions,语法与 GitHub Actions 兼容)
|
||||
#
|
||||
# 三个 job:
|
||||
# 1. backend-tests —— pytest 全量(sqlite+aiosqlite,无外部服务依赖;
|
||||
# OCC 契约测试在无 pythonocc 的 pip 环境自动 skip)
|
||||
# 2. frontend-build —— npm ci + npm run build(含 vue-tsc -b 类型检查,D15 已修复)
|
||||
# 3. openapi-drift —— 用 OCC 环境重导出 openapi.json 与仓库版本比对,
|
||||
# 防止接口变更三件套(AGENTS §2)被遗漏导致前后端漂移
|
||||
#
|
||||
# 运行前提:Gitea 实例启用 Actions 且注册了 runner;
|
||||
# `ubuntu-latest` label 需映射到带 node/git 的镜像(gitea runner 默认映射满足)。
|
||||
# 已验证事实:pytest 与 openapi 导出均不依赖 .env(settings 惰性校验)。
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
backend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install dependencies
|
||||
# requirements.txt 自带阿里云 pip 镜像配置
|
||||
run: pip install -r requirements.txt
|
||||
- name: Run tests
|
||||
run: python -m pytest tests/ -q
|
||||
|
||||
frontend-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
- name: Type check and build
|
||||
working-directory: frontend
|
||||
run: npm run build
|
||||
|
||||
openapi-drift:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Miniforge and pythonocc
|
||||
# pythonocc-core 仅经 conda-forge 提供(与 deploy/Dockerfile.moldinsight 同版本约束)
|
||||
# 默认走 TUNA 镜像(Gitea 服务器多为国内网络);海外环境可换回官方源
|
||||
run: |
|
||||
wget -q https://mirrors.tuna.tsinghua.edu.cn/github-release/conda-forge/miniforge/LatestRelease/Miniforge3-Linux-x86_64.sh -O miniforge.sh
|
||||
bash miniforge.sh -b -p "$HOME/miniforge"
|
||||
"$HOME/miniforge/bin/conda" create -n ci -c https://mirrors.tuna.tsinghua.edu.cn/conda-forge -y python=3.12 pythonocc-core=7.9.0
|
||||
- name: Install python dependencies
|
||||
run: "$HOME/miniforge/envs/ci/bin/pip" install -r requirements.txt
|
||||
- name: Export openapi.json and compare with committed version
|
||||
run: |
|
||||
"$HOME/miniforge/envs/ci/bin/python" -c "import sys; sys.path.insert(0, 'src'); from entrypoints.unified import app; import json; print(json.dumps(app.openapi(), ensure_ascii=False, indent=2))" > /tmp/openapi.json
|
||||
git diff --exit-code --no-index /tmp/openapi.json openapi.json || {
|
||||
echo "::error::openapi.json 与代码不一致——请按 docs/API_CONTRACT.md §4 重导出并执行 npm run gen:api"
|
||||
exit 1
|
||||
}
|
||||
+3
-1
@@ -15,7 +15,6 @@ __pycache__/
|
||||
# 项目临时文件
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
1panel.env
|
||||
*.env.local
|
||||
logs/
|
||||
@@ -24,6 +23,9 @@ logs/
|
||||
uploads/
|
||||
html_output/
|
||||
|
||||
# 前端构建产物(由 deploy.sh 在部署时 npm run build 生成,不进 git)
|
||||
static/
|
||||
|
||||
# 不再需要在 src/ 下跟踪这些
|
||||
src/uploads/
|
||||
src/html_output/
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# AGENTS.md - geMoldInsight 开发规范
|
||||
|
||||
> 本文件是给开发 agent(Claude / Codex / …)和协作开发者的入口文档。**开始任何实现前先读这个**,避免重复输入背景。
|
||||
> 人类入口见 [README.md](README.md);**当前实现状态见 [docs/STATUS.md](docs/STATUS.md)**(本文件不复制状态内容)。
|
||||
|
||||
## 1. 项目是什么
|
||||
|
||||
**geMoldInsight**:面向模具制造场景的综合系统,围绕 STEP/STP 模型分析、模具方案生成、分析结果沉淀与导出、成品创建、BOM / 库存 / 采购 / 销售闭环展开。
|
||||
|
||||
形态:**单仓库 + 单数据库 + 多模块 + 可独立部署的 modular monolith**。
|
||||
|
||||
- `moldinsight`:模具分析、几何处理、批量分析、成本估算、CAM、结果导出(Celery 异步链路)
|
||||
- `inventory`:成品 / 物料 / BOM / 库存 / 采购 / 销售 / 财务
|
||||
- `frontend`:Vue 3 独立前端工程(与后端同仓不同目录)
|
||||
- `shared`:配置、数据库、认证、日志、应用工厂等共享平台层
|
||||
|
||||
技术栈一句话:FastAPI + SQLAlchemy 2.0 + PostgreSQL + Alembic + Redis + Celery + PythonOCC/trimesh/pyvista + RustFS(MinIO 兼容);前端 Vue 3 + Vite + TypeScript + Pinia + TDesign;OpenAPI → TypeScript 类型生成。
|
||||
|
||||
架构与模块边界的详细说明见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
|
||||
|
||||
## 2. 硬约束速览(违反即返工)
|
||||
|
||||
- **文档先行**:非微小改动,先把实施方案写入对应文档,再按文档执行;方案变了先改文档再改代码。不能先改代码后补文档。
|
||||
- **完成需求后必须同步文档**:按 §4.1 的映射表逐项检查,防实现与文档漂移。
|
||||
- **每类信息只有一个归属文档**:状态只在 [docs/STATUS.md](docs/STATUS.md);架构边界只在 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md);规划只在 [docs/ROADMAP.md](docs/ROADMAP.md);技术债只在 [docs/TECH_DEBT.md](docs/TECH_DEBT.md);配置与运行只在 [docs/OPERATIONS.md](docs/OPERATIONS.md);部署入口只在 [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md);前后端契约只在 [docs/API_CONTRACT.md](docs/API_CONTRACT.md)。其他文档只链接,不复制。
|
||||
- **README 只做导航与最短入门**,不维护状态 / 架构 / 规划 / 部署细节。
|
||||
- **业务代码归属模块**:moldinsight 业务进 `src/moldinsight/`,inventory 业务进 `src/inventory/`;只有真正跨模块复用的基础能力才进 `src/shared/`。不继续把业务逻辑堆进 `shared`。
|
||||
- **单数据库是刻意设计**:moldinsight 与 inventory 共享同一 PostgreSQL(如 `STPFile.product_id -> Product.id` 桥接),不拆库。
|
||||
- **接口变更三件套**:优先用 Pydantic 请求模型(少用手写 `request.json()` 解析)→ 重新导出根目录 `openapi.json` → 前端 `npm run gen:api` 重新生成类型。三步缺一即契约漂移。
|
||||
- **历史材料统一进 [docs/archive/](docs/archive/README.md)**,不与当前权威文档混放。
|
||||
- **历史批次详细流水账 / 早段 STATUS**:见 [docs/archive/2026-09_governance_batches.md](docs/archive/2026-09_governance_batches.md) 与 [docs/archive/2026-09_status_history.md](docs/archive/2026-09_status_history.md);主骨架权威文档(TECH_DEBT §2 / STATUS 顶部)只保留摘要。
|
||||
- **配置只走 `.env`**(参照 [.env.example](.env.example) 全键说明):`DB_*`、`SECRET_KEY` 等关键项不设代码兜底(惰性校验,缺失即报),不在代码里给 localhost/弱口令默认值。
|
||||
|
||||
## 3. 代码地图
|
||||
|
||||
```
|
||||
src/
|
||||
entrypoints/ # 独立部署入口(纯组装:sys.path 修正 + create_app + startup_hooks/register_routers)
|
||||
moldinsight.py # moldinsight-only 入口:/api 前缀挂 moldinsight router,端口 8000
|
||||
inventory.py # inventory-only 入口:inventory_router,端口 8001
|
||||
unified.py # 双模块统一入口:/api 挂 moldinsight + inventory,当前推荐后端
|
||||
moldinsight/ # 【模具分析模块】
|
||||
api/
|
||||
__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 引用)
|
||||
health_router.py # /api/health 模块健康检查(含真实 pythonocc 探测与路由装载状态)
|
||||
upload_router.py # /api/upload STEP/STP 上传
|
||||
batch_router.py # /api/batch-upload 批量上传与分析
|
||||
task_router.py # /api/status/{task_id} 任务状态查询
|
||||
history_router.py # /api/history 分析历史与结果文件
|
||||
cam_router.py # /api/cam/plan CAM 加工方案(Pydantic 请求模型 + to_thread)
|
||||
design_router.py # 设计类接口:/optimize-layout /design-* /detect-undercuts(原 advanced_router,D1 拆分)
|
||||
cost_router.py # /cost-estimate 成本估算(原 advanced_router)
|
||||
machining_router.py # 加工类接口:/design-cam /check-collision /optimize-toolpath /design-electrodes /simulate-machining
|
||||
export_router.py # 导出类接口:/export-mold /export-download /export-recommendations
|
||||
core_modules.py # 核心计算模块惰性装载器(设计/加工路由共用,装载失败 503)
|
||||
aluminum_price_routes.py # /api/aluminum-price/* 铝价(模拟数据,响应带 source: "simulated",见 TECH_DEBT D2)
|
||||
html_report_router.py # GET /html/{filename} 报告代理(根路径挂载:RustFS 报告键 → 遗留 JSON 包装 → 本地卷兜底;include_into 由入口调用)
|
||||
debug_router.py # /api/debug/tasks 全量任务 dump(仅 DEBUG 模式注册,仍需登录)
|
||||
core/ # 几何与方案核心算法(OCC 重依赖区)
|
||||
stp_parser.py # STEP/STP 解析
|
||||
geometry_analyzer.py # 几何分析
|
||||
mesh_generator.py # 网格生成
|
||||
base_mold_generator.py # Generator 公共接口(契约测试覆盖)
|
||||
mold_generator.py # 模具生成
|
||||
mold_generator_registry.py # 生成器注册表
|
||||
aluminum_foam_mold.py # 铝泡沫模具方案
|
||||
feature_detector_registry.py # 特征识别注册表
|
||||
parting_candidate_generator.py / parting_scheme_scorer.py # 分模候选与评分
|
||||
multi_scheme_planner.py # 多方案规划
|
||||
mold_system_designer.py # 模架/浇注等系统设计
|
||||
side_action_designer.py # 侧向抽芯设计
|
||||
cavity_layout_optimizer.py # 型腔布局优化
|
||||
mold_machining.py / mold_cam.py # 加工与 CAM
|
||||
mold_quality_inspector.py # 质量检查
|
||||
cad_exporter.py # CAD 导出(export_mold_results 随方案 B 已删;export_step 等供 OCC 子进程持久化/转换)
|
||||
occ_worker.py # OCC 常驻工作进程入口:操作注册表(parse_stp/generate_mesh/generate_cavity/analyze_mold_design/detect_undercuts/convert_component_step/ping/sleep/warmup)+ worker_main 消息循环
|
||||
services/ # 业务服务层
|
||||
task_dispatcher.py # 后台任务统一分派(勿绕过它 fire-and-forget)
|
||||
task_query_service.py # 任务状态查询聚合
|
||||
processing_service.py # 分析处理编排(run_occ 经常驻 OCC 进程池调度,见 occ_process_pool.py / OCC_THROUGHPUT.md)
|
||||
occ_process_pool.py # OCC 常驻工作进程池(方案 B):超时/崩溃 terminate 换新补位;任务级超时 recover 整体重建
|
||||
calculation_service.py # 计算服务
|
||||
cost_estimate_service.py # 成本估算
|
||||
cam_bundle_service.py # CAM 结果打包
|
||||
verification_service.py # FreeCAD 验证(可选)
|
||||
stp_materializer.py # 按 task_id 把 STP 原件落盘临时文件(OCC 解析在子进程内,形状不跨进程)
|
||||
material_service.py # 物料价格服务
|
||||
aluminum_price_service.py # 铝价服务(模拟数据)
|
||||
llm_service.py # LLM 增强分析(可选,OpenAI 兼容)
|
||||
task_storage_service.py # STP 文件与处理任务生命周期存储(D9:数据写 flush-only,状态更新即时 commit)
|
||||
analysis_storage_service.py # 分析结果数据存储(几何/网格/型腔/HTML/特征)与任务数据视图组装
|
||||
file_history_service.py # 按文件名聚合的上传历史查询视图
|
||||
models/ # moldinsight 域 ORM(stp_analysis.py:stp_files 及各阶段产物 + processing_tasks,共 9 表)
|
||||
storage/
|
||||
rustfs_storage.py # RustFS/MinIO 客户端封装
|
||||
init_storage.py # RustFS 连接初始化 + rustfs_startup_hook(入口经 startup_hooks 注入)
|
||||
inventory/ # 【进销存模块】
|
||||
api/ # 每域一个 routes 文件:product / supplier / customer / warehouse / inventory / stock_movement / purchase_order / sales_order / purchase_demand / finance / dashboard / material
|
||||
schemas/ # 每域一个 Pydantic schema 文件(与 api 一一对应)
|
||||
services/ # 领域服务:inventory / master_data / material / product / purchase_order / sales_order / finance / purchase_demand / stock_movement / dashboard
|
||||
models/ # inventory 域 ORM(catalog / warehouse / trading / finance 四文件,共 15 表)
|
||||
utils.py
|
||||
shared/ # 【共享平台层:只放真正跨模块复用的基础能力,勿堆业务】
|
||||
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 惰性校验无默认
|
||||
database/database.py # async engine / session / get_db_session
|
||||
database/init_db.py # 建表与管理员种子
|
||||
models/base.py # 唯一 ORM Base + 模型归属约定(跨模块只许裸 FK,禁跨模块 relationship)
|
||||
models/identity.py # 身份与权限 ORM:User/Role/Permission/UserRole/RolePermission/UserActivity/SystemLog
|
||||
models/schemas.py # 共享 Pydantic 模型
|
||||
services/auth_routes.py # /api/auth/* 认证用户角色权限路由
|
||||
services/auth_service.py # JWT 签发校验 + get_current_active_user 依赖
|
||||
services/redis_task_manager.py # Redis 任务状态(Hash 字段级原子更新,兼容旧 string)
|
||||
utils/logger.py # 结构化日志(json/text)+ request_id
|
||||
utils/file_handler.py # 上传文件处理
|
||||
utils/html_generator.py # 可视化报告生成(HTML/摘要/数据 JSON;产物写任务临时目录,由 moldinsight 上传 RustFS 报告键)
|
||||
celery_app.py # Celery app(Redis broker,task_acks_late)
|
||||
celery_tasks.py # moldinsight 异步分析任务
|
||||
frontend/ # Vue 3 独立工程:src/modules 按域组织(moldinsight/inventory/users/login/home);src/types/api.ts 为 openapi 生成物,勿手改
|
||||
migrations/ # 数据库迁移
|
||||
scripts/ # 一次性迁移与工具脚本(migrations/ 数据迁移、db/ 索引与审计 SQL、tools/ 检查工具),非运行时代码
|
||||
tests/ # pytest:sqlite+aiosqlite 临时库;pythonocc 缺失时 OCC 契约测试自动 skip
|
||||
deploy/ # Dockerfile.* / nginx / build 脚本 / generate_lockfiles.{sh,bat}(D13 锁文件生成入口)
|
||||
docs/ # 权威文档(本文件 §5 导航)
|
||||
```
|
||||
|
||||
## 4. 开发约定
|
||||
|
||||
### 4.1 完成需求后的文档映射(改什么 → 同步什么)
|
||||
|
||||
| 变化 | 必须同步 |
|
||||
|---|---|
|
||||
| 实现状态(完成了什么 / 测试基线变化) | [docs/STATUS.md](docs/STATUS.md)(顶部加日志条目) |
|
||||
| 模块边界 / 目录结构 / 架构原则 | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) |
|
||||
| 接口路径 / 请求响应模型 / 鉴权 | [docs/API_CONTRACT.md](docs/API_CONTRACT.md) + 重新导出 `openapi.json` + `npm run gen:api` |
|
||||
| 配置项增删 / 启动方式 / 运维要求 | [docs/OPERATIONS.md](docs/OPERATIONS.md) + [.env.example](.env.example) |
|
||||
| 部署方式 / Compose / Nginx | [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)(操作细节进 [docs/deployment/LINUX_SETUP.md](docs/deployment/LINUX_SETUP.md)) |
|
||||
| 计划 / 优先级变化 | [docs/ROADMAP.md](docs/ROADMAP.md) |
|
||||
| 技术债新增 / 清偿 | [docs/TECH_DEBT.md](docs/TECH_DEBT.md) |
|
||||
| 阶段性结论 / 旧方案 | 迁入 [docs/archive/](docs/archive/README.md),不留在权威文档 |
|
||||
|
||||
### 4.2 测试
|
||||
|
||||
- 命令:`pytest tests/ -q`(pytest.ini 已定 `testpaths=tests`,asyncio auto 模式)。
|
||||
- 测试用 sqlite+aiosqlite 临时库([tests/conftest.py](tests/conftest.py) 自建 fixture),不依赖真实 PostgreSQL/Redis。
|
||||
- 依赖 pythonocc 的契约测试在无 OCC 环境自动 skip;OCC 全量验证用 conda 环境(参考项目实践:本地 pip 环境 + moldinsight conda/OCC 环境各跑一遍,基线数见 [docs/STATUS.md](docs/STATUS.md))。
|
||||
- 新增接口/服务逻辑应配套测试;改 `mold_generator` 公共接口必须保持契约测试通过。
|
||||
|
||||
### 4.3 API 与契约
|
||||
|
||||
- 新增 API 优先考虑模块归属(moldinsight / inventory / shared auth),路由文件过大按职责拆分。
|
||||
- 请求体用 Pydantic 模型定义,减少 `await request.json()` 手写解析(存量债务见 [docs/TECH_DEBT.md](docs/TECH_DEBT.md) D1)。
|
||||
- 接口变更后重导出 `openapi.json` 并在前端重新生成类型,步骤见 [docs/API_CONTRACT.md](docs/API_CONTRACT.md) §4。
|
||||
|
||||
### 4.4 配置
|
||||
|
||||
- 配置只走 `.env`([.env.example](.env.example) 为全键说明);compose 从同目录 `.env` 注入 `${VAR}`。
|
||||
- 关键项(`DB_*` / `SECRET_KEY` / `ADMIN_PASSWORD`)无代码兜底;新增硬依赖配置缺失要 fail-fast,不给 localhost 默认。
|
||||
- 配置项语义与加载优先级详见 [docs/OPERATIONS.md](docs/OPERATIONS.md) §1。
|
||||
|
||||
## 5. 文档导航
|
||||
|
||||
| 文档 | 管什么 |
|
||||
|---|---|
|
||||
| AGENTS.md(本文件) | agent briefing + 硬约束 + 代码地图 + 开发约定 |
|
||||
| [README.md](README.md) | 人类入口:是什么 + 快速启动 + 文档导航 |
|
||||
| [docs/STATUS.md](docs/STATUS.md) | **当前实现状态(唯一归属,常改,日志体)** |
|
||||
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | 架构 + 模块边界 + 结构原则 |
|
||||
| [docs/OPERATIONS.md](docs/OPERATIONS.md) | 配置 / 启动 / 环境 / 运维硬性要求 |
|
||||
| [docs/API_CONTRACT.md](docs/API_CONTRACT.md) | 前后端契约权威:端点 / 约定 / OpenAPI 类型生成 |
|
||||
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | 部署入口与部署文档分工 |
|
||||
| [docs/deployment/LINUX_SETUP.md](docs/deployment/LINUX_SETUP.md) | Linux 详细部署步骤 |
|
||||
| [docs/ROADMAP.md](docs/ROADMAP.md) | 演进路线与阶段计划 |
|
||||
| [docs/TECH_DEBT.md](docs/TECH_DEBT.md) | 活跃技术债与治理顺序 |
|
||||
| [docs/topics/](docs/topics/) | 专题补充(存储等) |
|
||||
| [docs/archive/README.md](docs/archive/README.md) | 历史文档归档入口 |
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
# Dockerfile for MoldInsight
|
||||
FROM continuumio/miniconda3:latest
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制项目文件
|
||||
COPY . .
|
||||
|
||||
# 更新conda并创建环境
|
||||
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
|
||||
|
||||
# 激活环境并安装Python依赖
|
||||
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
|
||||
|
||||
# 启动命令(使用shell形式确保环境激活)
|
||||
CMD ["/bin/bash", "-c", "source /opt/conda/etc/profile.d/conda.sh && conda activate moldinsight && python src/main.py"]
|
||||
@@ -1,4 +1,4 @@
|
||||
# Gemold - 模具制造管理系统
|
||||
# geMoldInsight
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -7,319 +7,199 @@
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 项目简介
|
||||
geMoldInsight 是一个面向模具制造场景的综合系统,围绕 **STEP/STP 模型分析、模具方案生成、分析结果沉淀、成品创建、BOM/库存/采购/销售闭环** 展开。
|
||||
|
||||
**Gemold** 是一个面向模具制造行业的综合性管理系统,集成了STP文件分析、模具设计建议、用户权限管理和进销存功能。 系统采用现代化的技术栈,提供高效、稳定、易扩展的解决方案。
|
||||
> **当前实现状态**:见 [docs/STATUS.md](docs/STATUS.md)
|
||||
> **当前架构与边界**:见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
||||
> **本文是唯一文档导航入口**:请按角色或主题跳转到对应主文档
|
||||
|
||||
### 核心功能
|
||||
---
|
||||
|
||||
| 功能模块 | 描述 |
|
||||
|---------|------|
|
||||
| 🔷 STP文件分析 | 使用PythonOCC解析STP文件,提取几何特征 |
|
||||
| 📊 模具设计建议 | 自动生成型腔、型芯、工艺参数等设计方案 |
|
||||
| 🎨 3D可视化 | 实时预览产品模型和模具结构 |
|
||||
| 👥 用户权限管理 | 完整的角色权限控制系统 |
|
||||
| 📦 进销存管理 | 库存、供应商、客户管理 |
|
||||
| 💾 数据持久化 | PostgreSQL + RustFS存储 |
|
||||
## 项目概览
|
||||
|
||||
当前项目已经从早期单体演进为:
|
||||
|
||||
- **moldinsight 模块**:模具分析、几何处理、批量分析、成本估算、结果导出
|
||||
- **inventory 模块**:产品、BOM、库存、采购、销售、财务
|
||||
- **frontend 模块**:Vue 3 前端工程
|
||||
- **shared 平台层**:配置、数据库、认证、日志、应用工厂
|
||||
|
||||
项目当前采用:
|
||||
|
||||
> **单仓库 + 单数据库 + 多模块 + 可独立部署**
|
||||
|
||||
更详细的结构说明见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始(三步)
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
前端开发需要:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 配置环境变量
|
||||
|
||||
复制并编辑:
|
||||
|
||||
- [`.env.example`](.env.example)
|
||||
- 部署场景可参考 [deploy/.env.example](deploy/.env.example)
|
||||
|
||||
### 3. 启动
|
||||
|
||||
推荐先查看部署入口:
|
||||
- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)
|
||||
|
||||
本地常见方式(按模式对应不同 compose 文件):
|
||||
|
||||
```bash
|
||||
# 默认:unified(前端 + 后端 + Celery)
|
||||
docker compose up -d
|
||||
|
||||
# 仅模具分析
|
||||
docker compose -f docker-compose.moldinsight.yml up -d
|
||||
|
||||
# 仅进销存
|
||||
docker compose -f docker-compose.inventory.yml up -d
|
||||
```
|
||||
|
||||
> 镜像首次构建:`bash deploy/build.sh`(build base → backend → frontend 3 个 tag,celery 复用 backend);更新代码后用 `docker compose up -d --build` 重建(裸 `up -d` 不会重建已有镜像)。
|
||||
|
||||
如需直接运行:
|
||||
|
||||
```bash
|
||||
uvicorn src.entrypoints.moldinsight:app --reload --host 0.0.0.0 --port 8000
|
||||
uvicorn src.entrypoints.inventory:app --reload --host 0.0.0.0 --port 8001
|
||||
```
|
||||
|
||||
> 如需 OCC 几何分析能力,请准备 PythonOCC 运行环境。项目中通常通过 conda 提供,而不是仅靠 pip 安装。
|
||||
|
||||
---
|
||||
|
||||
## 目录概览
|
||||
|
||||
```text
|
||||
geMoldInsight/
|
||||
├── src/
|
||||
│ ├── entrypoints/ # 独立部署入口
|
||||
│ ├── shared/ # 当前共享平台层
|
||||
│ ├── moldinsight/ # 模具分析模块
|
||||
│ ├── inventory/ # 进销存模块
|
||||
│ ├── celery_app.py # Celery app
|
||||
│ └── celery_tasks.py # moldinsight 异步任务
|
||||
├── frontend/ # 独立前端工程
|
||||
├── migrations/ # 数据库迁移
|
||||
├── deploy/ # 镜像、Nginx、部署辅助文件
|
||||
├── docs/
|
||||
├── tests/
|
||||
├── requirements.txt
|
||||
└── .env.example
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文档导航(唯一入口)
|
||||
|
||||
### 按主题阅读
|
||||
|
||||
| 文档 | 解决什么问题 |
|
||||
|---|---|
|
||||
| [AGENTS.md](AGENTS.md) | 项目开发规范、硬约束、代码地图、文档同步要求 |
|
||||
| [docs/STATUS.md](docs/STATUS.md) | 当前实现状态(日志体,唯一归属) |
|
||||
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | 当前架构、模块边界、结构原则 |
|
||||
| [docs/OPERATIONS.md](docs/OPERATIONS.md) | 配置 / 启动 / 环境 / 运维硬性要求 |
|
||||
| [docs/API_CONTRACT.md](docs/API_CONTRACT.md) | 前后端契约:端点总览、约定、OpenAPI 类型生成 |
|
||||
| [docs/ROADMAP.md](docs/ROADMAP.md) | 后续演进路线与阶段计划 |
|
||||
| [docs/TECH_DEBT.md](docs/TECH_DEBT.md) | 当前活跃技术债与治理计划 |
|
||||
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | 部署主题入口与部署文档分工 |
|
||||
| [docs/deployment/LINUX_SETUP.md](docs/deployment/LINUX_SETUP.md) | Linux 环境下的详细部署步骤 |
|
||||
| [docs/archive/BACKEND_MODULARIZATION_BLUEPRINT.md](docs/archive/BACKEND_MODULARIZATION_BLUEPRINT.md) | 模块化蓝图档案与补充设计讨论 |
|
||||
|
||||
### 按角色阅读
|
||||
|
||||
| 你是 | 建议阅读顺序 |
|
||||
|---|---|
|
||||
| 第一次了解项目 | 本文 → [docs/STATUS.md](docs/STATUS.md) → [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) |
|
||||
| 开发者 / 改代码 | [AGENTS.md](AGENTS.md) → [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) → [docs/API_CONTRACT.md](docs/API_CONTRACT.md) → [docs/TECH_DEBT.md](docs/TECH_DEBT.md) |
|
||||
| 运维 / 部署 | 本文 → [docs/OPERATIONS.md](docs/OPERATIONS.md) → [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) → [docs/deployment/LINUX_SETUP.md](docs/deployment/LINUX_SETUP.md) |
|
||||
| 规划 / 重构 | 本文 → [docs/ROADMAP.md](docs/ROADMAP.md) → [docs/TECH_DEBT.md](docs/TECH_DEBT.md) |
|
||||
|
||||
---
|
||||
|
||||
## 核心能力
|
||||
|
||||
| 模块 | 能力 |
|
||||
|---|---|
|
||||
| moldinsight | STEP/STP 上传、几何分析、特征识别、模具方案、批量分析、成本估算、结果导出 |
|
||||
| inventory | 成品/物料、BOM、库存、库存流水、采购订单、销售订单、财务、采购建议 |
|
||||
| integration | 分析结果一键创建成品,打通“模具分析 → 成品 → BOM → 销售/采购/库存” |
|
||||
| platform | 用户、角色、权限、JWT 鉴权、数据库连接、日志、健康检查 |
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端
|
||||
- **FastAPI** - 笷性能异步Web框架
|
||||
- **PythonOCC** - 专业CAD几何处理
|
||||
- **SQLAlchemy** - ORM框架
|
||||
- **PostgreSQL** - 关系型数据库
|
||||
- **RustFS** - 高性能文件存储
|
||||
- FastAPI
|
||||
- SQLAlchemy 2.0
|
||||
- PostgreSQL
|
||||
- Alembic
|
||||
- Redis
|
||||
- Celery
|
||||
- PythonOCC / trimesh / pyvista
|
||||
- RustFS / MinIO 兼容对象存储
|
||||
|
||||
### 前端
|
||||
- **Vue.js 3** - 渐进式JavaScript框架
|
||||
- **Three.js** - 3D可视化库
|
||||
- **原生CSS** - 白色简约现代风格
|
||||
- Vue 3
|
||||
- Vite
|
||||
- TypeScript
|
||||
- Pinia
|
||||
- Vue Router
|
||||
- TDesign Vue Next
|
||||
|
||||
### 基础设施
|
||||
- **Docker** - 容器化部署
|
||||
- **Systemd** - 服务管理
|
||||
- **Conda** - 环境管理
|
||||
- Docker / Docker Compose
|
||||
- 结构化日志 / request_id
|
||||
- OpenAPI → TypeScript 类型生成
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
## 当前代码入口
|
||||
|
||||
```
|
||||
geMoldInsight/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── main.py # 主程序入口
|
||||
│ ├── api/ # API路由层
|
||||
│ │ ├── routes.py # 模具分析API路由聚合
|
||||
│ │ ├── auth_routes.py # 认证API
|
||||
│ │ ├── v1/ # MoldInsight API (v1)
|
||||
│ │ │ ├── upload_router.py # 文件上传
|
||||
│ │ │ ├── task_router.py # 任务状态查询
|
||||
│ │ │ └── history_router.py # 分析历史
|
||||
│ │ └── inventory/ # 进销存API
|
||||
│ │ ├── product_routes.py # 产品管理
|
||||
│ │ ├── sales_order_routes.py # 销售订单
|
||||
│ │ ├── purchase_order_routes.py # 采购订单
|
||||
│ │ ├── finance_routes.py # 财务管理
|
||||
│ │ ├── material_routes.py # 物料管理
|
||||
│ │ └── schemas/ # 请求/响应模型
|
||||
│ ├── core/ # 核心业务逻辑
|
||||
│ │ ├── stp_parser.py # STP文件解析
|
||||
│ │ ├── geometry_analyzer.py # 几何分析
|
||||
│ │ ├── mesh_generator.py # 网格生成
|
||||
│ │ ├── mold_generator.py # 普通塑料模具生成
|
||||
│ │ ├── aluminum_foam_mold.py # 铝泡沫模具生成
|
||||
│ │ ├── mold_quality_inspector.py # 模具质量检测
|
||||
│ │ └── ai_mold_assistant.py # AI模具助手
|
||||
│ ├── models/ # 数据模型
|
||||
│ │ ├── database.py # SQLAlchemy ORM模型
|
||||
│ │ └── schemas.py # Pydantic模式
|
||||
│ ├── services/ # 服务层
|
||||
│ │ ├── auth_service.py # 认证服务
|
||||
│ │ ├── processing_service.py # STP处理流程编排
|
||||
│ │ ├── calculation_service.py # 工程参数计算
|
||||
│ │ ├── material_service.py # 材料属性管理
|
||||
│ │ ├── task_query_service.py # 任务状态查询
|
||||
│ │ ├── storage_integration_rustfs.py # RustFS存储集成
|
||||
│ │ ├── redis_task_manager.py # Redis任务管理
|
||||
│ │ └── verification_service.py # FreeCAD几何验证
|
||||
│ ├── database/ # 数据库管理
|
||||
│ │ ├── database.py # 数据库连接池
|
||||
│ │ ├── init_db.py # 数据库初始化
|
||||
│ │ └── migrate_db.py # 数据库迁移
|
||||
│ ├── storage/ # 对象存储
|
||||
│ │ ├── rustfs_storage.py # RustFS S3客户端
|
||||
│ │ └── object_storage.py # 通用对象存储
|
||||
│ └── utils/ # 工具类
|
||||
│ ├── logger.py # 日志工具
|
||||
│ ├── file_handler.py # 文件处理
|
||||
│ └── html_generator.py # 3D可视化HTML生成
|
||||
├── static/ # 前端静态资源
|
||||
│ ├── vue-app.js # Vue3 SPA应用
|
||||
│ ├── style.css # 全局样式
|
||||
│ └── index.html # 入口页面
|
||||
├── templates/ # Jinja2模板
|
||||
├── config/ # 配置模块
|
||||
│ └── settings.py # 环境变量配置
|
||||
├── scripts/ # 脚本工具
|
||||
│ ├── db/ # SQL脚本
|
||||
│ ├── migrations/ # 数据迁移脚本
|
||||
│ ├── tools/ # 临时检查/清理工具
|
||||
│ └── verify_stp.py # STP验证脚本
|
||||
├── docs/ # 项目文档
|
||||
│ └── deployment/ # 部署相关文档
|
||||
├── tests/ # 测试用例
|
||||
├── uploads/ # 上传文件目录 (gitignore)
|
||||
├── html_output/ # 3D可视化输出 (gitignore)
|
||||
├── requirements.txt # Python依赖
|
||||
├── docker-compose.yml # Docker编排配置
|
||||
├── Dockerfile # Docker构建文件
|
||||
├── start.sh # Linux启动脚本
|
||||
├── .env.example # 环境变量模板
|
||||
└── .env # 环境变量 (gitignore)
|
||||
```
|
||||
- unified: [src/entrypoints/unified.py](src/entrypoints/unified.py)
|
||||
- moldinsight-only: [src/entrypoints/moldinsight.py](src/entrypoints/moldinsight.py)
|
||||
- inventory-only: [src/entrypoints/inventory.py](src/entrypoints/inventory.py)
|
||||
|
||||
当前 Compose 入口(一键命令对应文件名):
|
||||
|
||||
- unified: [docker-compose.yml](docker-compose.yml) → `docker compose up -d`
|
||||
- moldinsight-only: [docker-compose.moldinsight.yml](docker-compose.moldinsight.yml) → `docker compose -f docker-compose.moldinsight.yml up -d`
|
||||
- inventory-only: [docker-compose.inventory.yml](docker-compose.inventory.yml) → `docker compose -f docker-compose.inventory.yml up -d`
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
## 开发建议
|
||||
|
||||
### 1. 环境要求
|
||||
- Python 3.10+
|
||||
- PostgreSQL 13+
|
||||
- Conda (推荐)
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
# 创建Conda环境
|
||||
conda create -n py_3.12 python=3.12
|
||||
|
||||
# 激活环境
|
||||
conda activate py_3.12
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 配置环境变量
|
||||
|
||||
创建 `.env` 文件:
|
||||
|
||||
```env
|
||||
# 数据库配置
|
||||
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/gemold
|
||||
|
||||
# 服务配置
|
||||
SECRET_KEY=your-secret-key-here
|
||||
PORT=8000
|
||||
|
||||
# 管理员配置
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin123
|
||||
```
|
||||
|
||||
### 4. 初始化数据库
|
||||
|
||||
```bash
|
||||
python src/database/init_db.py
|
||||
```
|
||||
|
||||
### 5. 启动服务
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
chmod +x start.sh
|
||||
./start.sh
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
start.bat
|
||||
```
|
||||
|
||||
**Docker:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 6. 访问服务
|
||||
|
||||
- 服务地址: http://localhost:8000
|
||||
- 默认管理员: admin / admin123
|
||||
|
||||
---
|
||||
|
||||
## 核心功能详解
|
||||
|
||||
### STP文件分析
|
||||
|
||||
系统使用PythonOCC解析STP文件,提取以下几何特征:
|
||||
|
||||
| 特征类型 | 描述 |
|
||||
|---------|------|
|
||||
| 边界框 | 产品整体尺寸 |
|
||||
| 体积 | 产品体积计算 |
|
||||
| 表面积 | 产品表面积计算 |
|
||||
| 壁厚 | 壁厚分布分析 |
|
||||
| 加强筋 | 加强筋位置和密度 |
|
||||
| 孔洞 | 孔洞和凹槽位置 |
|
||||
| 倒扣 | 倒扣区域检测 |
|
||||
| 对称性 | 对称性分析 |
|
||||
| 重心 | 重心位置计算 |
|
||||
|
||||
### 模具设计建议
|
||||
|
||||
系统自动生成以下设计建议。
|
||||
|
||||
| 建议类型 | 描述 |
|
||||
|---------|------|
|
||||
| 型腔数量 | 单腔或多腔建议 |
|
||||
| 模架尺寸 | 基于产品尺寸推荐 |
|
||||
| 顶出系统 | 顶针顶出布局 |
|
||||
| 冷却水路 | 冷却需求分析 |
|
||||
| 材料选择 | 基于产量推荐材料 |
|
||||
|
||||
### 3D可视化
|
||||
|
||||
- **点云模型** - 从STP提取的真实几何形状
|
||||
- **模具型腔** - 型腔和型芯可视化
|
||||
- **分型面** - 分型面位置显示
|
||||
- **交互控制** - 旋转、缩放、平移
|
||||
|
||||
- **视图切换** - 显示/隐藏各组件
|
||||
|
||||
---
|
||||
|
||||
## API文档
|
||||
|
||||
### 认证API
|
||||
|
||||
```
|
||||
POST /api/auth/login # 用户登录
|
||||
POST /api/auth/logout # 用户登出
|
||||
GET /api/auth/me # 获取当前用户信息
|
||||
```
|
||||
|
||||
### 文件分析API
|
||||
|
||||
```
|
||||
POST /api/upload # 上传STP文件
|
||||
POST /api/status/{task_id} # 获取分析状态
|
||||
GET /api/history # 获取分析历史
|
||||
```
|
||||
|
||||
### 进销存API
|
||||
|
||||
```
|
||||
GET /api/inventory/dashboard # 仪表盘数据
|
||||
GET /api/inventory/products # 产品列表
|
||||
POST /api/inventory/products # 创建产品
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署指南
|
||||
|
||||
### Systemd服务 (Linux)
|
||||
|
||||
```bash
|
||||
# 复制服务文件
|
||||
sudo cp gemoldinsight.service /etc/systemd/system/
|
||||
|
||||
# 启用服务
|
||||
sudo systemctl enable gemoldinsight
|
||||
|
||||
# 启动服务
|
||||
sudo systemctl start gemoldinsight
|
||||
```
|
||||
|
||||
### Docker部署
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t gemold:latest .
|
||||
|
||||
# 启动容器
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 代码风格
|
||||
- 遵循PEP 8规范
|
||||
- 使用类型注解
|
||||
- 保持函数简洁
|
||||
|
||||
### 提交规范
|
||||
- feat: 新功能
|
||||
- fix: 修复bug
|
||||
- docs: 文档更新
|
||||
- refactor: 代码重构
|
||||
- test: 测试相关
|
||||
- 新增业务逻辑优先放入对应业务模块,不要继续堆进 `shared`
|
||||
- 新增 API 时优先考虑模块归属,而不是“能放就放”
|
||||
- 文档状态统一维护在 [docs/STATUS.md](docs/STATUS.md)
|
||||
- 部署方式变化统一更新 [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件
|
||||
|
||||
---
|
||||
|
||||
## 贡献者
|
||||
|
||||
感谢所有为这个项目做出贡献的开发者。
|
||||
|
||||
---
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 项目地址: [GitHub](https://github.com/your-org/gemold)
|
||||
- 问题反馈: [Issues](https://github.com/your-org/gemold/issues)
|
||||
本项目采用 MIT 许可证,详见 [LICENSE](LICENSE)。
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = %(here)s/migrations
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
||||
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory. for multiple paths, the path separator
|
||||
# is defined by "path_separator" below.
|
||||
prepend_sys_path = .
|
||||
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the tzdata library which can be installed by adding
|
||||
# `alembic[tz]` to the pip requirements.
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to <script_location>/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "path_separator"
|
||||
# below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||
|
||||
# path_separator; This indicates what character is used to split lists of file
|
||||
# paths, including version_locations and prepend_sys_path within configparser
|
||||
# files such as alembic.ini.
|
||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||
# to provide os-dependent path splitting.
|
||||
#
|
||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||
# take place if path_separator is not present in alembic.ini. If this
|
||||
# option is omitted entirely, fallback logic is as follows:
|
||||
#
|
||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||
# behavior of splitting on spaces, commas, or colons.
|
||||
#
|
||||
# Valid values for path_separator are:
|
||||
#
|
||||
# path_separator = :
|
||||
# path_separator = ;
|
||||
# path_separator = space
|
||||
# path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||
# hooks = ruff
|
||||
# ruff.type = module
|
||||
# ruff.module = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration. This is also consumed by the user-maintained
|
||||
# env.py script only.
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
+35
-36
@@ -1,23 +1,43 @@
|
||||
# ============================================
|
||||
# 环境变量模板 — 独立部署版
|
||||
# 环境变量模板 — 复用服务器已有基础设施
|
||||
# ============================================
|
||||
# 复制为 .env 并修改配置
|
||||
# 复制为 .env 并按服务器实际服务地址修改
|
||||
|
||||
# 数据库
|
||||
DB_HOST=postgres
|
||||
# API / 前端对外端口
|
||||
FRONTEND_PORT=80
|
||||
BACKEND_PORT=8000
|
||||
MOLDINSIGHT_PORT=8000
|
||||
INVENTORY_PORT=8001
|
||||
|
||||
# 数据库(服务器已部署)
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_NAME=moldinsight
|
||||
DB_USER=moldinsight_user
|
||||
DB_PASSWORD=moldinsight_password
|
||||
DB_USER=moldinsight
|
||||
DB_PASSWORD=change-me
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=redis
|
||||
# Redis(服务器已部署)
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# MinIO (RustFS 兼容存储)
|
||||
MINIO_ACCESS_KEY=minioadmin
|
||||
MINIO_SECRET_KEY=minioadmin
|
||||
# RustFS / MinIO 兼容对象存储(服务器已部署)
|
||||
RUSTFS_ENDPOINT=http://127.0.0.1:9000
|
||||
RUSTFS_ACCESS_KEY=change-me
|
||||
RUSTFS_SECRET_KEY=change-me
|
||||
RUSTFS_TIMEOUT=30
|
||||
RUSTFS_PRESIGNED_URL_EXPIRES=3600
|
||||
|
||||
# 服务配置
|
||||
HOST=0.0.0.0
|
||||
DEBUG=false
|
||||
UPLOAD_DIR=./uploads
|
||||
MAX_FILE_SIZE=104857600
|
||||
ALLOWED_EXTENSIONS=.stp,.step,.stp.gz
|
||||
POINTCLOUD_SAMPLE_COUNT=10000
|
||||
MESH_QUALITY=high
|
||||
PARALLEL_PROCESSING=true
|
||||
|
||||
# 认证
|
||||
SECRET_KEY=your-secret-key-change-in-production-min-32-chars
|
||||
@@ -30,35 +50,14 @@ ADMIN_PASSWORD=change-this-to-a-secure-password
|
||||
ADMIN_EMAIL=admin@gemold.com
|
||||
ADMIN_FULL_NAME=系统管理员
|
||||
|
||||
# 端口
|
||||
MOLDINSIGHT_PORT=8000
|
||||
INVENTORY_PORT=8001
|
||||
# FreeCAD 验证(可选)
|
||||
ENABLE_FREECAD_VERIFICATION=false
|
||||
FREECAD_VERIFICATION_TIMEOUT=120
|
||||
|
||||
# 文件上传
|
||||
UPLOAD_DIR=./uploads
|
||||
MAX_FILE_SIZE=104857600
|
||||
ALLOWED_EXTENSIONS=.stp,.step,.stp.gz
|
||||
|
||||
# 几何处理
|
||||
POINTCLOUD_SAMPLE_COUNT=10000
|
||||
MESH_QUALITY=high
|
||||
PARALLEL_PROCESSING=true
|
||||
|
||||
# RustFS 对象存储
|
||||
RUSTFS_ENDPOINT=http://minio:9000
|
||||
RUSTFS_ACCESS_KEY=minioadmin
|
||||
RUSTFS_SECRET_KEY=minioadmin
|
||||
RUSTFS_TIMEOUT=30
|
||||
RUSTFS_PRESIGNED_URL_EXPIRES=3600
|
||||
|
||||
# LLM (可选)
|
||||
# LLM(可选)
|
||||
LLM_ENABLED=false
|
||||
LLM_API_URL=https://api.openai.com/v1
|
||||
LLM_API_KEY=sk-your-api-key
|
||||
LLM_MODEL=gpt-4o-mini
|
||||
LLM_TIMEOUT=60
|
||||
LLM_MAX_TOKENS=2000
|
||||
|
||||
# FreeCAD 验证 (可选)
|
||||
ENABLE_FREECAD_VERIFICATION=false
|
||||
FREECAD_VERIFICATION_TIMEOUT=120
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12-slim
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -13,5 +13,9 @@ RUN pip install --no-cache-dir -r requirements-base.txt && \
|
||||
|
||||
COPY src/shared/ /app/src/shared/
|
||||
|
||||
# init_db 启动期自动迁移(AUTO_MIGRATE)需要迁移脚本随镜像分发
|
||||
COPY migrations/ /app/migrations/
|
||||
COPY alembic.ini /app/alembic.ini
|
||||
|
||||
ENV PYTHONPATH=/app/src
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM gemold-moldinsight:latest
|
||||
|
||||
CMD ["celery", "-A", "celery_app", "worker", "--workdir=/app/src", "--concurrency=2", "--loglevel=info"]
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
# 注意:容器内 nginx listen 改为 8000(避免占用宿主 80;1024+ 无 root 限制)
|
||||
COPY deploy/nginx/frontend.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/frontend/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 8000
|
||||
@@ -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
|
||||
|
||||
@@ -1,24 +1,45 @@
|
||||
FROM continuumio/miniconda3:latest AS pythonocc
|
||||
# PythonOCC 仅经 conda-forge 提供,且其动态库与 conda Python 的 ABI 绑定。
|
||||
# 旧方式(conda 环境装好后把 site-packages 拷入 python:slim 系统 python)依赖
|
||||
# 两侧 Python ABI 恰好兼容,属脆弱做法(TECH_DEBT D13);现改为直接以同一
|
||||
# conda 运行时作为最终镜像的执行环境,自带全部动态库。
|
||||
# Base 用 Miniforge:conda-forge 默认且唯一渠道(无 defaults 渠道,无 Anaconda
|
||||
# ToS 顾虑),与 CI 的 Miniforge 安装(.gitea/workflows/ci.yml)同源;tag 锁定。
|
||||
FROM condaforge/miniforge3:24.7.1-2
|
||||
|
||||
RUN conda update -n base -c defaults conda -y && \
|
||||
conda create -n moldinsight python=3.12 pythonocc-core=7.9.0 -c conda-forge -y
|
||||
# 锁定几何栈核心版本;pip 侧全量版本锁由
|
||||
# `bash deploy/generate_lockfiles.sh` 在 moldinsight conda 环境内执行后生成
|
||||
# (D13,参见 deploy/generate_lockfiles.sh / .bat 与 docs/OPERATIONS.md §2)
|
||||
# 落盘产物为 deploy/requirements-{base,moldinsight}.lock.txt,CI / 离线构建可直接锁定安装
|
||||
RUN conda create -n moldinsight -c conda-forge -y \
|
||||
python=3.12 \
|
||||
pythonocc-core=7.9.0 \
|
||||
&& conda clean -afy
|
||||
|
||||
FROM gemold-base:latest
|
||||
ENV PATH=/opt/conda/envs/moldinsight/bin:$PATH \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
COPY --from=pythonocc /opt/conda/envs/moldinsight/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/
|
||||
WORKDIR /app
|
||||
|
||||
COPY deploy/requirements-moldinsight.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements-moldinsight.txt && \
|
||||
rm requirements-moldinsight.txt
|
||||
# 自包含构建:不再基于 gemold-base,基础依赖与模块依赖一并安装
|
||||
COPY deploy/requirements-base.txt deploy/requirements-moldinsight.txt ./
|
||||
RUN pip install --no-cache-dir \
|
||||
-r requirements-base.txt \
|
||||
-r requirements-moldinsight.txt \
|
||||
&& rm requirements-base.txt requirements-moldinsight.txt
|
||||
|
||||
COPY src/shared/ /app/src/shared/
|
||||
COPY src/moldinsight/ /app/src/moldinsight/
|
||||
COPY src/inventory/ /app/src/inventory/
|
||||
COPY src/entrypoints/ /app/src/entrypoints/
|
||||
COPY src/celery_app.py src/celery_tasks.py /app/src/
|
||||
|
||||
# init_db 启动期自动迁移(AUTO_MIGRATE)需要迁移脚本随镜像分发
|
||||
COPY migrations/ /app/migrations/
|
||||
COPY alembic.ini /app/alembic.ini
|
||||
|
||||
COPY uploads/ /app/uploads/
|
||||
COPY static/ /app/static/
|
||||
COPY html_output/ /app/html_output/
|
||||
|
||||
ENV PYTHONPATH=/app/src
|
||||
|
||||
RUN mkdir -p /app/logs
|
||||
|
||||
|
||||
+9
-15
@@ -8,25 +8,19 @@ 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 === 构建统一后端镜像 ===
|
||||
docker build -t gemold-backend:latest -f deploy\Dockerfile.moldinsight .
|
||||
|
||||
echo.
|
||||
echo === 构建 Inventory 镜像 ===
|
||||
docker build -t gemold-inventory:latest -f deploy\Dockerfile.inventory .
|
||||
|
||||
echo.
|
||||
echo === 构建 Celery Worker 镜像 ===
|
||||
docker build -t gemold-celery:latest -f deploy\Dockerfile.celery .
|
||||
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 up -d
|
||||
echo.
|
||||
echo 仅启动进销存:
|
||||
echo cd deploy ^&^& docker compose --profile inventory up -d
|
||||
echo.
|
||||
echo 仅启动模具分析:
|
||||
echo cd deploy ^&^& docker compose --profile moldinsight up -d
|
||||
echo 按文件名切换模式(旧 --profile 写法已失效):
|
||||
echo docker compose -f docker-compose.moldinsight.yml up -d
|
||||
echo docker compose -f docker-compose.inventory.yml up -d
|
||||
|
||||
+9
-14
@@ -10,25 +10,20 @@ 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 "=== 构建统一后端镜像 ==="
|
||||
docker build -t gemold-backend:latest -f deploy/Dockerfile.moldinsight .
|
||||
|
||||
echo ""
|
||||
echo "=== 构建 Inventory 镜像 ==="
|
||||
docker build -t gemold-inventory:latest -f deploy/Dockerfile.inventory .
|
||||
|
||||
echo ""
|
||||
echo "=== 构建 Celery Worker 镜像 ==="
|
||||
docker build -t gemold-celery:latest -f deploy/Dockerfile.celery .
|
||||
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 up -d"
|
||||
echo ""
|
||||
echo "仅启动进销存:"
|
||||
echo " cd deploy && docker compose --profile inventory up -d"
|
||||
echo ""
|
||||
echo "仅启动模具分析:"
|
||||
echo " cd deploy && docker compose --profile moldinsight up -d"
|
||||
echo "按文件名切换模式(旧 --profile 写法已失效):"
|
||||
echo " docker compose -f docker-compose.moldinsight.yml up -d"
|
||||
echo " docker compose -f docker-compose.inventory.yml up -d"
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: gemold_postgres
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-moldinsight}
|
||||
POSTGRES_USER: ${DB_USER:-moldinsight_user}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-moldinsight_password}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-moldinsight_user} -d ${DB_NAME:-moldinsight}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- gemold_network
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: gemold_redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- gemold_network
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: gemold_minio
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- gemold_network
|
||||
|
||||
moldinsight:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile.moldinsight
|
||||
image: gemold-moldinsight:latest
|
||||
container_name: gemold_moldinsight
|
||||
ports:
|
||||
- "${MOLDINSIGHT_PORT:-8000}:8000"
|
||||
environment:
|
||||
HOST: 0.0.0.0
|
||||
PORT: "8000"
|
||||
DB_HOST: postgres
|
||||
DB_PORT: "5432"
|
||||
DB_NAME: ${DB_NAME:-moldinsight}
|
||||
DB_USER: ${DB_USER:-moldinsight_user}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-moldinsight_password}
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: "6379"
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
|
||||
RUSTFS_ENDPOINT: http://minio:9000
|
||||
RUSTFS_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin}
|
||||
RUSTFS_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin}
|
||||
SECRET_KEY: ${SECRET_KEY:-change-me-in-production}
|
||||
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- full
|
||||
- moldinsight
|
||||
networks:
|
||||
- gemold_network
|
||||
|
||||
moldinsight-celery:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile.celery
|
||||
container_name: gemold_celery
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PORT: "5432"
|
||||
DB_NAME: ${DB_NAME:-moldinsight}
|
||||
DB_USER: ${DB_USER:-moldinsight_user}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-moldinsight_password}
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: "6379"
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
|
||||
RUSTFS_ENDPOINT: http://minio:9000
|
||||
RUSTFS_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin}
|
||||
RUSTFS_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin}
|
||||
RUSTFS_TIMEOUT: ${RUSTFS_TIMEOUT:-30}
|
||||
depends_on:
|
||||
- moldinsight
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- full
|
||||
- moldinsight
|
||||
networks:
|
||||
- gemold_network
|
||||
|
||||
inventory:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile.inventory
|
||||
image: gemold-inventory:latest
|
||||
container_name: gemold_inventory
|
||||
ports:
|
||||
- "${INVENTORY_PORT:-8001}:8001"
|
||||
environment:
|
||||
HOST: 0.0.0.0
|
||||
PORT: "8001"
|
||||
DB_HOST: postgres
|
||||
DB_PORT: "5432"
|
||||
DB_NAME: ${DB_NAME:-moldinsight}
|
||||
DB_USER: ${DB_USER:-moldinsight_user}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-moldinsight_password}
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: "6379"
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
|
||||
SECRET_KEY: ${SECRET_KEY:-change-me-in-production}
|
||||
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- full
|
||||
- inventory
|
||||
networks:
|
||||
- gemold_network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
minio_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
gemold_network:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,38 @@
|
||||
@echo off
|
||||
REM ============================================================
|
||||
REM geMoldInsight pip 锁文件生成脚本(TECH_DEBT D13,Windows 版)
|
||||
REM ============================================================
|
||||
REM
|
||||
REM 用法:在 gemold conda 环境内执行 deploy\generate_lockfiles.bat
|
||||
REM
|
||||
REM 产物:
|
||||
REM deploy\requirements-base.lock.txt
|
||||
REM deploy\requirements-moldinsight.lock.txt
|
||||
REM ============================================================
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
cd /d "%~dp0\.."
|
||||
|
||||
where conda >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] conda 未安装或不在 PATH,请先激活 conda 环境 ^(推荐 moldinsight^) 1>&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not "%CONDA_DEFAULT_ENV%"=="moldinsight" if not "%CONDA_DEFAULT_ENV%"=="gemold" (
|
||||
echo [WARN] 当前 conda 环境为 '%CONDA_DEFAULT_ENV%',推荐在 'moldinsight' 内执行
|
||||
)
|
||||
|
||||
echo ==^> 生成 base 锁文件
|
||||
pip freeze --exclude pythonocc-core > deploy\requirements-base.lock.txt
|
||||
|
||||
echo ==^> 生成 moldinsight 锁文件
|
||||
pip freeze --exclude pythonocc-core > deploy\requirements-moldinsight.lock.txt
|
||||
|
||||
echo.
|
||||
echo 已生成:
|
||||
echo deploy\requirements-base.lock.txt
|
||||
echo deploy\requirements-moldinsight.lock.txt
|
||||
echo.
|
||||
echo 下一步:提交两个 lock.txt,并按团队策略同步更新 requirements-*.txt 下限。
|
||||
endlocal
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# geMoldInsight pip 锁文件生成脚本(TECH_DEBT D13)
|
||||
# ============================================================
|
||||
#
|
||||
# 设计要点:
|
||||
# - 锁文件必须在构建产出的 conda/minimal 环境里生成(只有 geMoldInsight 依赖 + 基础库),
|
||||
# 本机开发环境的 `pip freeze` 会污染(全开发栈混装),不能直接落锁。
|
||||
# - 仅在带 moldinsight 的 conda 环境内运行 `pip freeze > deploy/requirements-<x>.lock.txt`
|
||||
# 才有意义。
|
||||
# - 锁文件落盘后即可被 CI / 离线构建 / 复现部署直接 `pip install -r` 锁定版本,
|
||||
# 而不再依赖 >= 下限解析。
|
||||
#
|
||||
# 用法(必须在 `gemold` conda 环境内执行):
|
||||
# bash deploy/generate_lockfiles.sh
|
||||
#
|
||||
# 产物:
|
||||
# deploy/requirements-base.lock.txt
|
||||
# deploy/requirements-moldinsight.lock.txt
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if ! command -v conda >/dev/null 2>&1; then
|
||||
echo "[ERROR] conda 未安装或不在 PATH,请先激活 conda 环境(推荐环境名 moldinsight)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${CONDA_DEFAULT_ENV:-}" != "moldinsight" && "${CONDA_DEFAULT_ENV:-}" != "gemold" ]]; then
|
||||
echo "[WARN] 当前 conda 环境为 '${CONDA_DEFAULT_ENV:-<未激活>}'," \
|
||||
"推荐在 'moldinsight' conda 环境内运行(否则锁文件将含宿主污染)" >&2
|
||||
fi
|
||||
|
||||
echo "==> 生成 base 锁文件"
|
||||
pip freeze --exclude pythonocc-core > deploy/requirements-base.lock.txt
|
||||
|
||||
echo "==> 生成 moldinsight 锁文件"
|
||||
pip freeze --exclude pythonocc-core > deploy/requirements-moldinsight.lock.txt
|
||||
|
||||
echo
|
||||
echo "已生成:"
|
||||
echo " deploy/requirements-base.lock.txt ($(wc -l < deploy/requirements-base.lock.txt) 行)"
|
||||
echo " deploy/requirements-moldinsight.lock.txt ($(wc -l < deploy/requirements-moldinsight.lock.txt) 行)"
|
||||
echo
|
||||
echo "下一步:"
|
||||
echo " 1. 提交这两个 lock.txt(仅含项目直接依赖 + conda-minimal 环境产出)"
|
||||
echo " 2. 同步更新 deploy/requirements-base.txt / requirements-moldinsight.txt 的版本下限" \
|
||||
"为 lock 中的实际版本(或保留 >=,按团队策略)"
|
||||
@@ -0,0 +1,53 @@
|
||||
upstream gemold_backend_upstream {
|
||||
server backend:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8000;
|
||||
server_name _;
|
||||
|
||||
# 与 .env MAX_FILE_SIZE=104857600(100MB)对齐;nginx 默认 1m 会直接 413
|
||||
client_max_body_size 100M;
|
||||
|
||||
# 大文件上传给后端足够时间(默认 60s,100MB 可能不够)
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ minio>=7.1.0
|
||||
aiohttp>=3.13.4
|
||||
|
||||
celery[redis]>=5.3.0
|
||||
kafka-python>=2.0.2
|
||||
|
||||
pytest>=7.0.0
|
||||
pytest-asyncio>=0.21.0
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# geMoldInsight — inventory-only 模式
|
||||
#
|
||||
# 一键启动:
|
||||
# docker compose -f docker-compose.inventory.yml up -d
|
||||
#
|
||||
# 服务清单:仅 inventory 后端
|
||||
# 不含前端、不含 moldinsight、不含 Celery worker。
|
||||
#
|
||||
# 基础设施:仅依赖 PostgreSQL + Redis;不依赖 RustFS / MinIO 对象存储,
|
||||
# 不挂 uploads/html 命名卷(inventory 无文件分析链路)。
|
||||
# 注意:服务未声明 profiles(避免裸 up 报 "no service selected"),
|
||||
# 模式切换唯一入口是 -f 文件名。
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 公共环境变量(inventory 子集:DB / Redis / 认证 / 启动参数)
|
||||
# ---------------------------------------------------------------
|
||||
x-inventory-env: &inventory_env
|
||||
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:-}
|
||||
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY 未配置:请在 .env 中设置}
|
||||
ALGORITHM: ${ALGORITHM:-HS256}
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-1440}
|
||||
DEBUG: ${DEBUG:-false}
|
||||
SERVE_FRONTEND_STATIC: ${SERVE_FRONTEND_STATIC:-false}
|
||||
|
||||
services:
|
||||
inventory:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/Dockerfile.inventory
|
||||
image: gemold-inventory:latest
|
||||
container_name: gemold_inventory
|
||||
command: ["python", "-m", "uvicorn", "entrypoints.inventory:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||
ports:
|
||||
- "${INVENTORY_PORT}:8001"
|
||||
environment:
|
||||
<<: *inventory_env
|
||||
HOST: 0.0.0.0
|
||||
PORT: "8001"
|
||||
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD 未配置:请在 .env 中设置}
|
||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@gemold.com}
|
||||
ADMIN_FULL_NAME: ${ADMIN_FULL_NAME:-系统管理员}
|
||||
AUTO_MIGRATE: ${AUTO_MIGRATE:-true}
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- gemold_network
|
||||
|
||||
networks:
|
||||
gemold_network:
|
||||
name: gemold_network
|
||||
driver: bridge
|
||||
@@ -0,0 +1,115 @@
|
||||
# geMoldInsight — moldinsight-only 模式
|
||||
#
|
||||
# 一键启动:
|
||||
# docker compose -f docker-compose.moldinsight.yml up -d
|
||||
#
|
||||
# 服务清单:moldinsight(独立 API)+ moldinsight-celery(异步分析 worker)
|
||||
# 不含前端、不含 inventory。
|
||||
# 镜像:两服务共用 gemold-backend:latest(同一 build 声明,compose 只构建一次;
|
||||
# celery 仅以 command 覆盖启动 worker),无跨镜像构建依赖,干净机器裸 up 一把过。
|
||||
# 注意:服务未声明 profiles(避免裸 up 报 "no service selected"),
|
||||
# 模式切换唯一入口是 -f 文件名。
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 公共环境变量(moldinsight 家族共用)
|
||||
# ---------------------------------------------------------------
|
||||
x-base-env: &base_env
|
||||
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:?SECRET_KEY 未配置:请在 .env 中设置}
|
||||
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}
|
||||
|
||||
services:
|
||||
moldinsight:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/Dockerfile.moldinsight
|
||||
# 统一用 gemold-backend tag(历史 gemold-moldinsight:latest 双 tag 已废弃)
|
||||
image: gemold-backend:latest
|
||||
container_name: gemold_moldinsight
|
||||
command: ["python", "-m", "uvicorn", "entrypoints.moldinsight:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
ports:
|
||||
- "${MOLDINSIGHT_PORT}:8000"
|
||||
environment:
|
||||
<<: *base_env
|
||||
HOST: 0.0.0.0
|
||||
PORT: "8000"
|
||||
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD 未配置:请在 .env 中设置}
|
||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@gemold.com}
|
||||
ADMIN_FULL_NAME: ${ADMIN_FULL_NAME:-系统管理员}
|
||||
AUTO_MIGRATE: ${AUTO_MIGRATE:-true}
|
||||
# uploads_data:D6 过渡兜底,RustFS 异常时本地路径回退
|
||||
# html_data:D11 后仅作 /html 报告代理的存量兜底读(新产物不落本地)
|
||||
volumes:
|
||||
- uploads_data:/app/uploads
|
||||
- html_data:/app/html_output
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- gemold_network
|
||||
|
||||
moldinsight-celery:
|
||||
# 与 moldinsight 共用同一镜像,仅 command 覆盖启动 worker
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/Dockerfile.moldinsight
|
||||
image: gemold-backend:latest
|
||||
container_name: gemold_celery
|
||||
# --pool=solo:任务在 celery 主进程内执行。prefork 的 worker 子进程是
|
||||
# daemonic,multiprocessing 禁止守护进程再生子进程,OCC 常驻进程池
|
||||
# (OccProcessPool spawn 拉起)在 prefork 下必报
|
||||
# "daemonic processes are not allowed to have children"(2026-09-26)。
|
||||
# solo 天然单任务串行,与模块级单例(asyncio.Lock 不可跨循环并发)匹配;
|
||||
# 需要吞吐时横向加容器副本,而不是调并发。
|
||||
command: ["sh", "-c", "cd /app/src && exec celery -A celery_app worker --pool=solo --loglevel=info"]
|
||||
environment:
|
||||
<<: *base_env
|
||||
# 旧 CELERY_CONCURRENCY / CELERY_MAX_TASKS_PER_CHILD 已随 --pool=solo 移除:
|
||||
# solo 单进程串行,二者不适用(.env 里残留定义会被 compose 静默忽略)
|
||||
volumes:
|
||||
- uploads_data:/app/uploads
|
||||
# 注:本文件无 backend service,celery 必须等 moldinsight API 就绪后再启动
|
||||
depends_on:
|
||||
- moldinsight
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- gemold_network
|
||||
|
||||
networks:
|
||||
gemold_network:
|
||||
name: gemold_network
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
uploads_data:
|
||||
name: gemold_uploads_data
|
||||
html_data:
|
||||
name: gemold_html_data
|
||||
+131
-106
@@ -1,119 +1,144 @@
|
||||
# docker-compose.yml - 完整版(包含PostgreSQL和MinIO)
|
||||
# geMoldInsight — unified 模式(默认入口)
|
||||
#
|
||||
# 一键启动:
|
||||
# docker compose up -d
|
||||
#
|
||||
# 其他模式(换文件名即可,无需 --profile):
|
||||
# docker compose -f docker-compose.moldinsight.yml up -d
|
||||
# docker compose -f docker-compose.inventory.yml up -d
|
||||
#
|
||||
# 注意:服务均未声明 profiles(compose 规则:声明了 profiles 的服务在
|
||||
# 不带 --profile 时不会被选中,裸 up 会报 "no service selected"),
|
||||
# 旧 --profile 写法不再是模式开关,模式切换唯一入口是 -f 文件名。
|
||||
#
|
||||
# 服务清单:frontend + backend(unified 入口) + moldinsight-celery
|
||||
# 镜像:gemold-backend / gemold-frontend(celery 复用 gemold-backend,仅 command 不同)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 公共环境变量(moldinsight 家族 backend/celery/moldinsight 共用)
|
||||
# 通过 anchor 在本文件内复用,避免 environment 块重复
|
||||
# ---------------------------------------------------------------
|
||||
x-base-env: &base_env
|
||||
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 必须在 .env 中显式配置,否则 compose 直接失败
|
||||
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY 未配置:请在 .env 中设置}
|
||||
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}
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: moldinsight_postgres
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-moldinsight}
|
||||
POSTGRES_USER: ${DB_USER:-moldinsight_user}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-moldinsight_password}
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/Dockerfile.frontend
|
||||
image: gemold-frontend:latest
|
||||
container_name: gemold_frontend
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-moldinsight_user} -d ${DB_NAME:-moldinsight}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- moldinsight_network
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: moldinsight_minio
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
|
||||
ports:
|
||||
- "9000:9000" # API端口
|
||||
- "9001:9001" # 控制台端口
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- moldinsight_network
|
||||
|
||||
moldinsight:
|
||||
build: .
|
||||
container_name: moldinsight_app
|
||||
ports:
|
||||
- "${HOST_PORT:-10001}:${CONTAINER_PORT:-8000}" # 宿主机端口:容器端口
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./html_output:/app/html_output
|
||||
- ./logs:/app/logs
|
||||
- ./.env:/app/.env:ro
|
||||
environment:
|
||||
# 应用端口配置(从settings.py读取)
|
||||
- HOST=${HOST:-0.0.0.0}
|
||||
- PORT=${CONTAINER_PORT:-8000}
|
||||
# 数据库配置
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=${DB_NAME:-moldinsight}
|
||||
- DB_USER=${DB_USER:-moldinsight_user}
|
||||
- DB_PASSWORD=${DB_PASSWORD:-moldinsight_password}
|
||||
# RustFS配置
|
||||
- RUSTFS_ENDPOINT=http://minio:9000
|
||||
- RUSTFS_ACCESS_KEY=${MINIO_ACCESS_KEY:-minioadmin}
|
||||
- RUSTFS_SECRET_KEY=${MINIO_SECRET_KEY:-minioadmin}
|
||||
# 浏览器入口端口(无默认值:必须由 .env 中 FRONTEND_PORT 显式配置)
|
||||
- "${FRONTEND_PORT}:8000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
networks:
|
||||
- moldinsight_network
|
||||
- gemold_network
|
||||
|
||||
celery-worker:
|
||||
build: .
|
||||
container_name: moldinsight_celery
|
||||
command: >
|
||||
/bin/bash -c "source /opt/conda/etc/profile.d/conda.sh && conda activate moldinsight && cd /app/src && celery -A celery_app worker --concurrency=2 --loglevel=info"
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./html_output:/app/html_output
|
||||
- ./logs:/app/logs
|
||||
- ./.env:/app/.env:ro
|
||||
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"]
|
||||
# 宿主机端口由 .env 的 BACKEND_PORT 决定:空/不设则不暴露宿主机端口
|
||||
# (仅经前端 /api 反代同域访问,避免与 frontend 抢占宿主 10003)。
|
||||
# 调试时设 BACKEND_PORT=10003 即可独立访问。
|
||||
ports:
|
||||
- "${BACKEND_PORT:-}:8000"
|
||||
environment:
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=${DB_NAME:-moldinsight}
|
||||
- DB_USER=${DB_USER:-moldinsight_user}
|
||||
- DB_PASSWORD=${DB_PASSWORD:-moldinsight_password}
|
||||
- RUSTFS_ENDPOINT=http://minio:9000
|
||||
- RUSTFS_ACCESS_KEY=${MINIO_ACCESS_KEY:-minioadmin}
|
||||
- RUSTFS_SECRET_KEY=${MINIO_SECRET_KEY:-minioadmin}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
<<: *base_env
|
||||
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD 未配置:请在 .env 中设置}
|
||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@gemold.com}
|
||||
ADMIN_FULL_NAME: ${ADMIN_FULL_NAME:-系统管理员}
|
||||
AUTO_MIGRATE: ${AUTO_MIGRATE:-true}
|
||||
# 共享卷过渡兜底(D6/D11):主链路已改走 RustFS。uploads 供 RustFS 异常时
|
||||
# 本地路径回退;html_output 仅作 /html 报告代理的存量兜底读(新产物不落本地)
|
||||
volumes:
|
||||
- uploads_data:/app/uploads
|
||||
- html_data:/app/html_output
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- moldinsight_network
|
||||
- gemold_network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
minio_data:
|
||||
driver: local
|
||||
moldinsight-celery:
|
||||
# 与 backend 共用同一镜像(同一 build 声明 + 同一 tag,compose 只构建一次),
|
||||
# 仅以 command 覆盖启动 worker——消除旧 Dockerfile.celery(已删)FROM
|
||||
# gemold-backend 在并行构建下的"镜像尚不存在"陷阱,干净机器裸 up 一把过
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/Dockerfile.moldinsight
|
||||
image: gemold-backend:latest
|
||||
container_name: gemold_celery
|
||||
# cd /app/src 是必须的:celery_app / celery_tasks 是 src/ 的子模块,
|
||||
# 而其 import 用的是裸模块名(celery_app.py 内 include=["celery_tasks"]),
|
||||
# 所以 worker 必须从 src/ 目录启动;不依赖 celery 版本是否支持 --workdir
|
||||
# --pool=solo:任务在 celery 主进程内执行。prefork 的 worker 子进程是
|
||||
# daemonic,multiprocessing 禁止守护进程再生子进程,OCC 常驻进程池
|
||||
# (OccProcessPool spawn 拉起)在 prefork 下必报
|
||||
# "daemonic processes are not allowed to have children"(2026-09-26)。
|
||||
# solo 天然单任务串行,与 processing_service / redis_task_manager /
|
||||
# db_manager 等模块级单例(其 asyncio.Lock 不可跨循环并发)也匹配;
|
||||
# 需要吞吐时横向加容器副本,而不是调并发。
|
||||
command: ["sh", "-c", "cd /app/src && exec celery -A celery_app worker --pool=solo --loglevel=info"]
|
||||
environment:
|
||||
<<: *base_env
|
||||
# 旧 CELERY_CONCURRENCY / CELERY_MAX_TASKS_PER_CHILD 已随 --pool=solo 移除:
|
||||
# solo 单进程串行,二者不适用(.env 里残留定义会被 compose 静默忽略)
|
||||
# uploads_data 共享卷(D6 过渡兜底):RustFS 异常时 worker 回退本地路径下载。
|
||||
# D11 后 worker 不再写 HTML 产物(直传 RustFS 报告键),无需 html_data 卷
|
||||
volumes:
|
||||
- uploads_data:/app/uploads
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- gemold_network
|
||||
|
||||
networks:
|
||||
moldinsight_network:
|
||||
gemold_network:
|
||||
# 固定 name,便于跨 compose 文件调试时容器互通(如 inventory-only 与 unified 临时联调)
|
||||
name: gemold_network
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
uploads_data:
|
||||
name: gemold_uploads_data
|
||||
html_data:
|
||||
name: gemold_html_data
|
||||
@@ -0,0 +1,113 @@
|
||||
# geMoldInsight 前后端契约(API_CONTRACT)
|
||||
|
||||
> 文档定位:**前后端契约的唯一归属**——端点总览、统一约定、OpenAPI 类型生成流程。
|
||||
> 端点定义、路径、请求/响应 schema 的**最终真相源是根目录 `openapi.json`**(由 FastAPI 自动生成);本文维护人可读的总览与变更规则。当前状态见 [STATUS.md](STATUS.md),架构见 [ARCHITECTURE.md](ARCHITECTURE.md),接口类技术债见 [TECH_DEBT.md](TECH_DEBT.md) D1。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总览
|
||||
|
||||
三种部署形态暴露的 API 面(入口见 [src/entrypoints/](../src/entrypoints/)):
|
||||
|
||||
| 形态 | API 面 |
|
||||
|---|---|
|
||||
| unified(推荐) | `/api/*`(moldinsight + inventory)+ `/api/auth/*` + 顶层 `/health` |
|
||||
| moldinsight-only | `/api/*`(moldinsight)+ `/api/auth/*` + `/health` |
|
||||
| inventory-only | `/api/*`(inventory)+ `/api/auth/*` + `/health` |
|
||||
|
||||
- moldinsight 路由在 [moldinsight/api/\_\_init\_\_.py](../src/moldinsight/api/__init__.py) 按 `ROUTE_MODULES` 清单经 `_safe_include` 聚合(装载失败登记 [route_registry.py](../src/moldinsight/api/route_registry.py),`/api/health` 呈现 `degraded` 并列出失败模块;`DEBUG=true` 下失败直接抛错;`debug_router` 仅 `DEBUG=true` 注册)。
|
||||
- inventory 路由在 [inventory/api/\_\_init\_\_.py](../src/inventory/api/__init__.py) 按域静态聚合。
|
||||
- 认证路由来自 [shared/services/auth_routes.py](../src/shared/services/auth_routes.py),由 [app_factory](../src/shared/app_factory.py) 挂载,三种形态共用。
|
||||
|
||||
## 2. 统一约定
|
||||
|
||||
- **鉴权**:JWT Bearer(`Authorization: Bearer <token>`)。登录:`POST /api/auth/login`(表单)/ `POST /api/auth/login/json`(JSON);受保护路由通过 FastAPI 依赖 `get_current_active_user` 注入当前用户([shared/services/auth_service.py](../src/shared/services/auth_service.py))。`SECRET_KEY` 跨进程必须一致。
|
||||
- **响应形态**:现状**无统一信封包装**——各端点直接返回业务 JSON;schema 以 `openapi.json` 的 components 为准。新增接口不建议另起信封风格,保持与所在模块一致。
|
||||
- **错误**:FastAPI 标准 `HTTPException` 状态码语义;业务校验优先 Pydantic 请求模型自动 422。
|
||||
- **业务路由前缀**:全部业务端点在 `/api` 下;顶层仅 `/health`(探活)、`/html/{filename}`(可视化报告代理)、`/login`、`/users`(历史遗留入口,前端主链路用 `/api/auth/*`)。
|
||||
|
||||
## 3. 端点总览(按域分组)
|
||||
|
||||
> 下表为导航用速览;路径参数、请求/响应字段以 `openapi.json` 为准。
|
||||
|
||||
### 3.1 认证与用户(shared)
|
||||
|
||||
| 域 | 端点 | 文件 |
|
||||
|---|---|---|
|
||||
| 登录 | `/api/auth/login`、`/api/auth/login/json`、`/api/auth/logout` | auth_routes.py |
|
||||
| 当前用户 | `/api/auth/me` | auth_routes.py |
|
||||
| 用户管理 | `/api/auth/users`、`/api/auth/users/{user_id}`、`/api/auth/users/{user_id}/reset-password`(管理员;JSON body `{ new_password }`,最短 6 位) | auth_routes.py |
|
||||
| 角色权限 | `/api/auth/roles`、`/api/auth/roles/{role_id}`、`/api/auth/roles/{role_id}/permissions`、`/api/auth/permissions`、`/api/auth/permissions/{permission_id}` | auth_routes.py |
|
||||
|
||||
### 3.2 moldinsight(模具分析)
|
||||
|
||||
| 域 | 端点 | 文件 |
|
||||
|---|---|---|
|
||||
| 上传 | `/api/upload` | upload_router.py |
|
||||
| 批量分析 | `/api/batch-upload`、`/api/batch/{batch_id}`(聚合状态以 PG 为准;响应含 `current_step`;他人批次 403、不存在 404) | batch_router.py |
|
||||
| 任务状态 | `/api/status/{task_id}`(需登录;仅任务所有者可访问,他人/无主任务 403,不存在 404) | task_router.py |
|
||||
| 历史结果 | `/api/history`、`/api/history/{filename}` | history_router.py |
|
||||
| HTML 报告 | `/html/{filename}`(根路径,非 `/api` 前缀;D11 代理:RustFS 报告键 `html/reports/{filename}` 直取 → 遗留 JSON 包装对象 → 本地 `html_output/` 存量兜底 → 404。已知约束:不做认证——iframe 无法携带 Authorization 头,沿用 StaticFiles 时代既定姿态) | html_report_router.py |
|
||||
| CAM | `/api/cam/plan`(Pydantic 请求模型;未提供的偏好回落任务持久化偏好再回落默认) | cam_router.py |
|
||||
| 设计 | `/api/optimize-layout`、`/api/design-cooling`、`/api/design-gating`、`/api/design-mold-system`、`/api/detect-undercuts` | design_router.py |
|
||||
| 成本估算 | `/api/cost-estimate` | cost_router.py |
|
||||
| 加工 | `/api/design-cam`、`/api/check-collision`、`/api/optimize-toolpath`、`/api/design-electrodes`、`/api/simulate-machining` | machining_router.py |
|
||||
| 导出 | `/api/export-mold`、`/api/export-download/{filepath}`、`/api/export-recommendations` | export_router.py |
|
||||
| 铝价(模拟数据) | `/api/aluminum-price/current`、`/api/aluminum-price/history` | aluminum_price_routes.py |
|
||||
| 老师傅经验反馈(D17) | `/api/tasks/{task_id}/experience-feedback`(写入:需登录 + 任务归属 + `feedback_experience_hint` 权限;body 含 `scheme_id` / `feedback_status ∈ {adopted, adjust, rejected}` / 可选 `feedback_reason` / `adjust_suggestion` / 上下文快照;写完调用 `TaskQueryService.invalidate_task_view`);`/api/tasks/{task_id}/experience-hints`(读取:需登录 + 任务归属;返回同 stp_file_id + material_family + is_foam 锚定的历史 hints 聚合,按 scheme_axis 分组,含 adopted/rejected/adjust 计数 + 加权 confidence + sample_count + 回显 fingerprint) | experience_feedback_router.py |
|
||||
| 健康检查 | `/api/health`(有路由装载失败时 `status: degraded` 并列出失败清单;`pythonocc` 为真实探测) | health_router.py |
|
||||
| 调试(仅 DEBUG) | `/api/debug/tasks` | debug_router.py |
|
||||
|
||||
### 3.3 inventory(进销存)
|
||||
|
||||
| 域 | 端点 | 文件 |
|
||||
|---|---|---|
|
||||
| 成品 | `/api/products`、`/api/products/{product_id}`、`/api/products/{product_id}/materials`、`/api/products/from-task/{task_id}` | product_routes.py |
|
||||
| 物料价格/供应商 | `/api/materials/*` | material_routes.py |
|
||||
| 供应商 | `/api/suppliers`、`/api/suppliers/{supplier_id}` | supplier_routes.py |
|
||||
| 客户 | `/api/customers`、`/api/customers/{customer_id}` | customer_routes.py |
|
||||
| 仓库 | `/api/warehouses` | warehouse_routes.py |
|
||||
| 库存 | `/api/inventory`、`/api/inventory/{inventory_id}` | inventory_routes.py |
|
||||
| 库存流水 | `/api/stock-movements` | stock_movement_routes.py |
|
||||
| 采购订单 | `/api/purchase-orders`、`/api/purchase-orders/{order_id}`、`.../receive`、`.../status` | purchase_order_routes.py |
|
||||
| 采购建议 | `/api/purchase-demands/calculate` | purchase_demand_routes.py |
|
||||
| 销售订单 | `/api/sales-orders`、`/api/sales-orders/{order_id}`、`.../status`、`.../issue-materials`、`.../consume-materials`、`.../production-plan` | sales_order_routes.py |
|
||||
| 财务 | `/api/finance/summary`、`/api/finance/receivables|payables|receipts|payments|transactions`、`/api/finance/partner-statement/{partner_type}` 等 | finance_routes.py |
|
||||
| 看板 | `/api/dashboard` | dashboard_routes.py |
|
||||
|
||||
**moldinsight → inventory 桥接**:`/api/products/from-task/{task_id}` 由分析任务一键创建成品(对应 `STPFile.product_id -> Product.id` 单库桥接,见 [ARCHITECTURE.md](ARCHITECTURE.md) §5.1)。
|
||||
|
||||
## 4. OpenAPI 与前端类型生成(接口变更三件套)
|
||||
|
||||
接口变更后**必须依次完成**:
|
||||
|
||||
1. **改代码**:路由 + Pydantic 请求/响应模型(优先模型,少写 `request.json()` 解析)。
|
||||
2. **重导出 `openapi.json`**(在可 import 项目的环境执行,如 conda `gemold`):
|
||||
|
||||
```bash
|
||||
python -c "import sys; sys.path.insert(0, 'src'); from entrypoints.unified import app; import json; print(json.dumps(app.openapi(), ensure_ascii=False, indent=2))" > openapi.json
|
||||
```
|
||||
|
||||
(moldinsight-only 契约视角可把 `entrypoints.unified` 换成 `entrypoints.moldinsight`;对外主契约以 unified 为准。)
|
||||
|
||||
3. **重新生成前端类型**:
|
||||
|
||||
```bash
|
||||
cd frontend && npm run gen:api # openapi-typescript:../openapi.json -> src/types/api.ts
|
||||
```
|
||||
|
||||
- `frontend/src/types/api.ts` 是**生成物,禁止手改**;前端代码类型引用它。
|
||||
- 三步缺一即前后端契约漂移(硬约束,见 [AGENTS.md](../AGENTS.md) §2)。
|
||||
- 当前 `openapi.json` 于 2026-09-17 随批次 3 重导出(76 paths),前端 `src/types/api.ts` 同步再生。
|
||||
|
||||
## 5. 契约变更规则
|
||||
|
||||
- 新增接口先定模块归属(moldinsight / inventory / shared auth),再写路由;返回结构、路径、鉴权发生变化时,同步更新本文相应表格。
|
||||
- 路由文件过大按职责拆分(参照批次 3 的 design / cost / machining / export 拆分先例;新增路由须登记 [moldinsight/api/\_\_init\_\_.py](../src/moldinsight/api/__init__.py) 的 `ROUTE_MODULES`)。
|
||||
- 破坏性变更(删字段 / 改语义)需在 [STATUS.md](STATUS.md) 日志条目中记录,并确认前端同仓同步修改。
|
||||
|
||||
## 6. 前端消费约定
|
||||
|
||||
- 前端为独立工程 [frontend/](../frontend/)(Vue 3 + Vite + TS + Pinia + TDesign),按域组织在 `src/modules/`(moldinsight / inventory / users / login / home)。
|
||||
- API 类型唯一来源 `src/types/api.ts`(生成物);组件不手写与后端重复的响应类型。
|
||||
- 跨域:开发期走 Vite;部署期为同域反代(见 [DEPLOYMENT.md](DEPLOYMENT.md)),`CORS_ORIGINS` 仅服务于非同域场景。
|
||||
@@ -0,0 +1,312 @@
|
||||
# geMoldInsight 架构与边界(ARCHITECTURE)
|
||||
|
||||
> 文档定位:**当前架构、模块边界与结构原则的权威文档**。
|
||||
> 本文描述“现在的系统结构是什么、边界如何划分、目标形态是什么”;不负责维护当前实现进度,当前状态见 [STATUS.md](STATUS.md),部署见 [DEPLOYMENT.md](DEPLOYMENT.md),演进路线见 [ROADMAP.md](ROADMAP.md)。
|
||||
> 如需追溯模块化设计蓝图与扩展讨论,见 [archive/BACKEND_MODULARIZATION_BLUEPRINT.md](archive/BACKEND_MODULARIZATION_BLUEPRINT.md)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构目标
|
||||
|
||||
geMoldInsight 的目标架构不是微服务,也不是继续维持历史单体,而是:
|
||||
|
||||
> **单仓库 + 单数据库 + 多模块 + 可独立部署的 modular monolith**
|
||||
|
||||
这意味着:
|
||||
|
||||
- 保持同一 Git 仓库
|
||||
- 保持同一 PostgreSQL 数据库
|
||||
- 按模块组织业务代码与部署入口
|
||||
- 在不拆库、不拆仓的前提下,明确业务边界与部署边界
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前模块划分
|
||||
|
||||
### 2.1 moldinsight
|
||||
|
||||
目录:
|
||||
- [src/moldinsight/](../src/moldinsight/)
|
||||
|
||||
职责:
|
||||
- STEP/STP 上传与任务管理
|
||||
- 几何分析与特征识别
|
||||
- 模具方案生成
|
||||
- 批量分析
|
||||
- 成本估算
|
||||
- 导出与结果查询
|
||||
- 结合 Celery 执行异步分析链路
|
||||
|
||||
### 2.2 inventory
|
||||
|
||||
目录:
|
||||
- [src/inventory/](../src/inventory/)
|
||||
|
||||
职责:
|
||||
- 成品/物料管理
|
||||
- BOM 管理
|
||||
- 库存与库存流水
|
||||
- 采购订单 / 销售订单
|
||||
- 财务与对账
|
||||
- 采购建议推导
|
||||
|
||||
### 2.3 frontend
|
||||
|
||||
目录:
|
||||
- [frontend/](../frontend/)
|
||||
|
||||
职责:
|
||||
- 模具分析页面
|
||||
- 进销存页面
|
||||
- 登录/用户管理
|
||||
- 统一路由与状态管理
|
||||
- 基于 OpenAPI 类型生成的前端调用
|
||||
|
||||
### 2.4 shared(当前平台层)
|
||||
|
||||
目录:
|
||||
- [src/shared/](../src/shared/)
|
||||
|
||||
职责:
|
||||
- 配置
|
||||
- 数据库连接与 session
|
||||
- 认证与权限
|
||||
- 日志与 request_id
|
||||
- 应用工厂与共用中间件
|
||||
|
||||
说明:
|
||||
- `shared` 的定位已明确为**平台层(跨模块基础能力)**;模块专属接线(RustFS 启动 / HTML 报告挂载 / 路由聚合)已收敛回模块层(见 §6.2),当前仓库结构仍以 `shared` 为事实名称
|
||||
- 剩余语义收敛(identity 平台表 vs 模块表的命名与注释口径)随实际重构继续推进
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前代码结构
|
||||
|
||||
当前核心结构如下:
|
||||
|
||||
```text
|
||||
geMoldInsight/
|
||||
├── src/
|
||||
│ ├── entrypoints/
|
||||
│ │ ├── moldinsight.py
|
||||
│ │ └── inventory.py
|
||||
│ ├── shared/
|
||||
│ ├── moldinsight/
|
||||
│ ├── inventory/
|
||||
│ ├── celery_app.py
|
||||
│ └── celery_tasks.py
|
||||
├── frontend/
|
||||
├── migrations/
|
||||
├── deploy/
|
||||
├── docs/
|
||||
└── tests/
|
||||
```
|
||||
|
||||
这反映的是**当前实际代码组织**,不是历史单体结构。
|
||||
|
||||
---
|
||||
|
||||
## 4. 部署边界
|
||||
|
||||
当前设计上支持三种部署模式:
|
||||
|
||||
### 4.1 unified
|
||||
|
||||
一个统一后端同时承载 moldinsight + inventory,并作为前端默认反代目标。
|
||||
|
||||
适合:
|
||||
- 本地开发
|
||||
- 集成环境
|
||||
- 小团队统一部署
|
||||
|
||||
### 4.2 moldinsight-only
|
||||
|
||||
只部署模具分析后端。
|
||||
|
||||
适合:
|
||||
- 单独开放分析能力
|
||||
- 异步任务与文件处理独立扩容
|
||||
|
||||
入口:
|
||||
- [src/entrypoints/moldinsight.py](../src/entrypoints/moldinsight.py)
|
||||
|
||||
### 4.3 inventory-only
|
||||
|
||||
只部署进销存后端。
|
||||
|
||||
适合:
|
||||
- 独立使用 ERP / 库存能力
|
||||
- 与 moldinsight 分开部署节奏
|
||||
|
||||
入口:
|
||||
- [src/entrypoints/inventory.py](../src/entrypoints/inventory.py)
|
||||
|
||||
部署操作与当前推荐方案见 [DEPLOYMENT.md](DEPLOYMENT.md)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键架构原则
|
||||
|
||||
### 5.1 单数据库是刻意设计
|
||||
|
||||
项目不是把 moldinsight 与 inventory 强拆成两个数据库,而是保留共享数据库,以支撑完整业务闭环:
|
||||
|
||||
- 分析结果
|
||||
- 创建成品
|
||||
- 成品 BOM
|
||||
- 销售 / 采购 / 库存
|
||||
|
||||
典型桥接关系示例:
|
||||
- `STPFile.product_id -> Product.id`
|
||||
|
||||
桥接只允许**裸 FK 列**(字符串表名),**不允许跨模块 ORM relationship**——单模块部署下另一模块的模型类可能未注册,跨模块 relationship 会让 mapper 配置直接失败(2026-09-17 批次 4 起为硬规则,原三条跨模块 relationship 均无使用方,已删除;对象化查询由使用方显式 select)。
|
||||
|
||||
### 5.2 模块边界优先于“临时方便”
|
||||
|
||||
新增逻辑时,应优先放入对应业务模块,而不是继续堆进 `shared`。
|
||||
|
||||
原则上:
|
||||
- moldinsight 业务进入 `src/moldinsight/`
|
||||
- inventory 业务进入 `src/inventory/`
|
||||
- 只有真正跨模块复用的基础能力才进入 `src/shared/`
|
||||
|
||||
### 5.3 前端是独立工程,不是后端静态附属
|
||||
|
||||
前端已是独立 Vite/Vue 工程,部署上可与后端组合,但在代码组织上应视为独立模块,而不是后端 `static/` 的扩展。
|
||||
|
||||
---
|
||||
|
||||
## 6. 当前主要耦合点
|
||||
|
||||
虽然模块化已经成型,但仍有几个关键耦合点需要持续关注:
|
||||
|
||||
### 6.1 共享 ORM 模型 —— 已按模块拆分(2026-09-17,批次 4)
|
||||
|
||||
历史上的 `shared/models/database.py`(31 个模型类三类同居)已拆除,现为按归属分置:
|
||||
|
||||
- [src/shared/models/base.py](../src/shared/models/base.py):唯一 `Base` + 归属约定与全量注册点说明
|
||||
- [src/shared/models/identity.py](../src/shared/models/identity.py):用户/角色/权限/审计(平台层,所有部署形态共用)
|
||||
- [src/moldinsight/models/](../src/moldinsight/models/):STEP 分析域 9 表(stp_files 及各阶段产物、processing_tasks)
|
||||
- [src/inventory/models/](../src/inventory/models/):进销存 15 表(catalog / warehouse / trading / finance 四域文件)
|
||||
|
||||
跨模块只允许裸 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 组合职责 —— 已收敛(2026-09-18,D3 剩余)
|
||||
|
||||
平台层与模块专属接线的边界已明确(`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.4 D17 Human-in-Loop 老师傅经验反馈闭环 —— 已完成(2026-09-23~24,3 个 commit)
|
||||
|
||||
算法演进由老师傅经验驱动:通过方案级整体反馈(采纳 / 建议调整 / 拒绝)按"产品指纹 + 工艺参数"为键跨任务匹配,下次同指纹产品分析自动消费老师傅沉淀的经验。这是少数"算法层由用户在线学习样本持续校准"的端到端闭环。
|
||||
|
||||
**端到端数据流**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 老师傅在 ResultView 点 👍 老师傅反馈按钮 │
|
||||
│ → HumanFeedbackDialog 三选一(采纳 / 建议调整 / 拒绝) │
|
||||
└──────────────────────────────┬──────────────────────────────┘
|
||||
│ POST /api/tasks/{id}/experience-feedback
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ experience_feedback_router (src/moldinsight/api/) │
|
||||
│ - ensure_task_access 归属校验 │
|
||||
│ - current_user.has_permission("feedback_experience_hint") │
|
||||
│ - service.record_feedback (flush; commit + invalidate) │
|
||||
└──────────────────────────────┬───────────────────────────────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ experience_feedback_service.record_feedback │
|
||||
│ - compute_fingerprint (bbox_aspect / volume_bucket / │
|
||||
│ face_bucket / undercut_class / material_family / is_foam) │
|
||||
│ - 写 experience_feedback 表(D9 边界 / D17 衰减 90d TTL) │
|
||||
│ - 同 stp_file_id 整体续期 expires_at │
|
||||
└──────────────────────────────┬───────────────────────────────┘
|
||||
│ 同 stp_file_id 上传新 STP 自动消费
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ processing_service._step_generate_cavity │
|
||||
│ - experience_feedback_service.resolve_for_process_params │
|
||||
│ → hints (List[{scheme_axis, weight, sample_count, ...}]) │
|
||||
│ - hints 装进 run_occ payload 顶层 experience_hints │
|
||||
└──────────────────────────────┬───────────────────────────────┘
|
||||
│ OCC 子进程(spawn 隔离)
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ occ_worker._op_generate_cavity │
|
||||
│ - payload.get("experience_hints") or {} → planner.generate_plan(hints=...) │
|
||||
└──────────────────────────────┬───────────────────────────────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ MultiSchemeMoldPlanner.generate_plan(..., hints=None) │
|
||||
│ - candidate_generator.generate_candidates(..., hints) │
|
||||
│ * priority_score += weight × 20 │
|
||||
│ * sample_count ≥ 2 + weight ≥ 0.5 → method="human_experience_primary" │
|
||||
│ - scheme_scorer.score_schemes(schemes, *, hints) │
|
||||
│ * score_breakdown["human_hint_bonus"] = weight × 12 │
|
||||
│ (sample_count < 2 时 ×0.5 折半) │
|
||||
│ - global_summary.applied_hints 注入返回 │
|
||||
└──────────────────────────────┬───────────────────────────────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ ResultView 渲染: │
|
||||
│ - summary-header 加 t-tag theme="success" 📚 历史经验 N 条 │
|
||||
│ - 反馈提交后 onFeedbackSubmitted → loadExperienceHints 即刷 │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**硬规则遵守**:
|
||||
|
||||
- 跨模块 FK 仍守 §5.1(`experience_feedback.user_id` / `processing_task_id` / `stp_file_id` 全用字符串表名,无 ORM relationship)
|
||||
- OCC 跨进程守 [occ_worker.py:7-8](../src/moldinsight/core/occ_worker.py#L7-L8) "杜绝 pickle OCC 对象"——payload 普通 dict 透传
|
||||
- D9 边界不破:service.flush + 路由 commit(无 service 内 commit)
|
||||
- 现有 `init_db.py` 幂等修复:按 code 补登权限/角色
|
||||
|
||||
**重量级约束**:
|
||||
|
||||
- `weight = max(0, (adopted-rejected)/total)`:仅正向有效,老师傅拒绝不"扣分"老算法
|
||||
- `sample_count < 2` 时 bonus ×0.5:信号不足折半,但 priority_score 仍加成(候选方向仍偏向)
|
||||
- 解析失败回退空 list:graceful,主流程不因下游错误退化
|
||||
- `canGiveFeedback` 角色门控:`is_superuser || roles 含 process_engineer`
|
||||
|
||||
**测试基线**:192 passed, 13 skipped(批 3 净增 +4 OCC-gated:candidate_generator 3 / scheme_scorer 4 / multi_scheme_planner 2 / processing_service 2);前端 vue-tsc + vite 通过。
|
||||
|
||||
详见 [TECH_DEBT.md](TECH_DEBT.md) D17 + [STATUS.md](STATUS.md) 2026-09-23~24 日志。
|
||||
|
||||
---
|
||||
|
||||
## 7. 专题文档与主骨架的关系
|
||||
|
||||
以下文档仍可作为专题补充参考,但不再承担默认入口职责:
|
||||
|
||||
- 存储方向:
|
||||
- [topics/storage/RUSTFS_STORAGE.md](topics/storage/RUSTFS_STORAGE.md)
|
||||
- [topics/storage/STORAGE_SETUP.md](topics/storage/STORAGE_SETUP.md)
|
||||
- 性能方向:
|
||||
- [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md)(OCC 吞吐与隔离方案设计,TECH_DEBT D10 归属)
|
||||
|
||||
AI、铝泡沫等更偏历史设计/规划性质的专题材料已迁入 [archive/README.md](archive/README.md)。
|
||||
|
||||
阶段性任务清单、迁移计划、历史总结等文档会逐步迁入 [archive/README.md](archive/README.md)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 与相关文档的边界
|
||||
|
||||
- 想看“当前做到哪一步”:看 [STATUS.md](STATUS.md)
|
||||
- 想看“后面还要往哪演进”:看 [ROADMAP.md](ROADMAP.md)
|
||||
- 想看“当前有哪些活跃技术债”:看 [TECH_DEBT.md](TECH_DEBT.md)
|
||||
- 想看“配置怎么给、服务怎么起”:看 [OPERATIONS.md](OPERATIONS.md)
|
||||
- 想看“前后端接口契约”:看 [API_CONTRACT.md](API_CONTRACT.md)
|
||||
- 想看“怎么部署”:看 [DEPLOYMENT.md](DEPLOYMENT.md)
|
||||
- 想看“模块化蓝图与更完整设计讨论”:看 [archive/BACKEND_MODULARIZATION_BLUEPRINT.md](archive/BACKEND_MODULARIZATION_BLUEPRINT.md)
|
||||
@@ -1,390 +0,0 @@
|
||||
# 铝制家电包装泡沫模具分模功能开发检查清单
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **文档名称** | 铝制家电包装泡沫模具分模功能开发检查清单 |
|
||||
| **版本** | 1.0 |
|
||||
| **日期** | 2026-03-13 |
|
||||
| **项目** | geMoldInsight 模具分模功能增强 |
|
||||
|
||||
---
|
||||
|
||||
## 检查清单说明
|
||||
|
||||
本文档包含铝制家电包装泡沫模具分模功能开发的完整检查清单,用于跟踪开发进度和验收标准。
|
||||
|
||||
---
|
||||
|
||||
## 第一阶段:基础框架搭建
|
||||
|
||||
### 任务 1.1:创建铝泡沫模具参数类
|
||||
|
||||
- [ ] 创建 `AluminumFoamMoldParams` 数据类
|
||||
- [ ] 添加发泡倍率参数 (`expansion_ratio`)
|
||||
- [ ] 添加目标密度参数 (`target_density`)
|
||||
- [ ] 添加成型温度参数 (`molding_temp`)
|
||||
- [ ] 添加收缩率参数 (`shrinkage_rate`)
|
||||
- [ ] 实现参数验证方法
|
||||
- [ ] 设置合理的默认值
|
||||
|
||||
### 任务 1.2:创建铝泡沫材料数据库
|
||||
|
||||
- [ ] 创建 `FoamMaterialDatabase` 类
|
||||
- [ ] 添加 AlSi10Mg 材料数据
|
||||
- [ ] 添加 AlSi12 材料数据
|
||||
- [ ] 添加纯铝泡沫材料数据
|
||||
- [ ] 实现材料查询方法
|
||||
- [ ] 实现材料参数获取方法
|
||||
|
||||
### 任务 1.3:扩展现有模具生成器
|
||||
|
||||
- [ ] 修改 `MoldCavityGenerator` 构造函数
|
||||
- [ ] 添加铝泡沫参数支持
|
||||
- [ ] 添加 `set_foam_material()` 方法
|
||||
- [ ] 更新 `generate_mold_cavities()` 流程
|
||||
- [ ] 适配收缩补偿计算
|
||||
- [ ] 适配拔模角计算
|
||||
|
||||
### 任务 1.4:创建参数配置 API 接口
|
||||
|
||||
- [ ] 添加获取默认参数 API
|
||||
- [ ] 添加设置参数 API
|
||||
- [ ] 添加参数验证 API
|
||||
- [ ] 添加模板保存 API
|
||||
- [ ] 添加模板加载 API
|
||||
- [ ] 实现请求参数解析
|
||||
|
||||
### 任务 1.5:前端参数面板开发
|
||||
|
||||
- [ ] 创建参数配置组件
|
||||
- [ ] 添加分模参数输入控件
|
||||
- [ ] 添加铝泡沫参数输入控件
|
||||
- [ ] 添加拔模参数输入控件
|
||||
- [ ] 添加高级参数折叠面板
|
||||
- [ ] 实现参数提交功能
|
||||
- [ ] 实现参数加载功能
|
||||
|
||||
### 任务 1.6:参数模板功能
|
||||
|
||||
- [ ] 创建预设模板(快速模式)
|
||||
- [ ] 创建预设模板(经济模式)
|
||||
- [ ] 创建预设模板(高精度模式)
|
||||
- [ ] 实现模板保存功能
|
||||
- [ ] 实现模板加载功能
|
||||
- [ ] 实现模板列表功能
|
||||
|
||||
### 任务 1.7:参数验证逻辑
|
||||
|
||||
- [ ] 实现分型精度范围验证
|
||||
- [ ] 实现型腔匹配度验证
|
||||
- [ ] 实现拔模角范围验证
|
||||
- [ ] 实现收缩率范围验证
|
||||
- [ ] 实现温度范围验证
|
||||
- [ ] 返回详细错误信息
|
||||
|
||||
### 任务 1.8:阶段一集成测试
|
||||
|
||||
- [ ] 测试参数设置功能
|
||||
- [ ] 测试参数获取功能
|
||||
- [ ] 测试参数验证
|
||||
- [ ] 测试模板保存/加载
|
||||
- [ ] 测试前端参数面板
|
||||
- [ ] 测试 API 接口响应
|
||||
- [ ] 无阻塞性 bug
|
||||
|
||||
---
|
||||
|
||||
## 第二阶段:分模算法优化
|
||||
|
||||
### 任务 2.1:改进法向量分析算法
|
||||
|
||||
- [ ] 添加高斯权重计算
|
||||
- [ ] 实现多点采样
|
||||
- [ ] 优化主方向识别
|
||||
- [ ] 处理法向量突变
|
||||
- [ ] 测试复杂几何产品
|
||||
|
||||
### 任务 2.2:实现多分型面检测
|
||||
|
||||
- [ ] 设计多分型面数据结构
|
||||
- [ ] 实现分型面优先级排序
|
||||
- [ ] 实现分型面序列生成
|
||||
- [ ] 处理分型面交叠
|
||||
- [ ] 测试 2 分型面案例
|
||||
- [ ] 测试 3+ 分型面案例
|
||||
|
||||
### 任务 2.3:倒扣区域检测
|
||||
|
||||
- [ ] 分析产品几何特征
|
||||
- [ ] 实现倒扣识别算法
|
||||
- [ ] 标记倒扣位置
|
||||
- [ ] 生成倒扣报告
|
||||
- [ ] 提供处理建议
|
||||
|
||||
### 任务 2.4:改进拔模角计算
|
||||
|
||||
- [ ] 集成 BRepOffsetAPI_DraftAngle
|
||||
- [ ] 实现拔模方向检测
|
||||
- [ ] 实现拔模干涉检测
|
||||
- [ ] 处理拔模失败情况
|
||||
- [ ] 验证拔模后尺寸
|
||||
|
||||
### 任务 2.5:铝泡沫收缩补偿
|
||||
|
||||
- [ ] 基于发泡倍率计算收缩
|
||||
- [ ] 实现多向收缩
|
||||
- [ ] 补偿后尺寸验证
|
||||
- [ ] 处理不均匀收缩
|
||||
|
||||
### 任务 2.6:型腔分离优化
|
||||
|
||||
- [ ] 优化布尔运算参数
|
||||
- [ ] 处理复杂几何
|
||||
- [ ] 添加分离结果验证
|
||||
- [ ] 处理分离失败回退
|
||||
|
||||
### 任务 2.7:模具块生成
|
||||
|
||||
- [ ] 计算模具尺寸
|
||||
- [ ] 添加安全余量
|
||||
- [ ] 生成 A/B 板结构
|
||||
- [ ] 添加模架结构
|
||||
- [ ] 验证模具强度
|
||||
|
||||
### 任务 2.8:分型线平滑处理
|
||||
|
||||
- [ ] 实现 B 样条拟合
|
||||
- [ ] 处理尖角
|
||||
- [ ] 保持几何精度
|
||||
- [ ] 验证平滑效果
|
||||
|
||||
### 任务 2.9:算法性能优化
|
||||
|
||||
- [ ] 添加并行计算
|
||||
- [ ] 优化缓存策略
|
||||
- [ ] 性能测试 < 30 秒
|
||||
- [ ] 内存测试 < 1GB
|
||||
|
||||
### 任务 2.10:阶段二集成测试
|
||||
|
||||
- [ ] 测试典型产品分模
|
||||
- [ ] 测试复杂产品分模
|
||||
- [ ] 测试多分型面产品
|
||||
- [ ] 测试算法稳定性
|
||||
- [ ] 性能达标
|
||||
|
||||
---
|
||||
|
||||
## 第三阶段:质量检测模块
|
||||
|
||||
### 任务 3.1:创建质量检测器类
|
||||
|
||||
- [ ] 创建 `MoldQualityInspector` 类
|
||||
- [ ] 定义检测接口
|
||||
- [ ] 设计结果数据结构
|
||||
- [ ] 实现批量检测
|
||||
|
||||
### 任务 3.2:分模面平滑度检测
|
||||
|
||||
- [ ] 实现曲率分析
|
||||
- [ ] 实现凹凸检测
|
||||
- [ ] 计算平滑度评分
|
||||
- [ ] 标记问题区域
|
||||
|
||||
### 任务 3.3:分模面连续性检测
|
||||
|
||||
- [ ] 实现边界检查
|
||||
- [ ] 实现间隙检测
|
||||
- [ ] 实现完整性验证
|
||||
- [ ] 报告问题位置
|
||||
|
||||
### 任务 3.4:模具结构合理性检测
|
||||
|
||||
- [ ] 实现模具尺寸检查
|
||||
- [ ] 实现壁厚检查
|
||||
- [ ] 实现干涉检查
|
||||
- [ ] 生成改进建议
|
||||
|
||||
### 任务 3.5:生产可行性评估
|
||||
|
||||
- [ ] 计算注塑压力
|
||||
- [ ] 计算锁模力
|
||||
- [ ] 估算成型周期
|
||||
- [ ] 评估生产成本
|
||||
|
||||
### 任务 3.6:质量报告生成
|
||||
|
||||
- [ ] 汇总检测结果
|
||||
- [ ] 生成文字说明
|
||||
- [ ] 添加图表
|
||||
- [ ] 支持 PDF 导出
|
||||
- [ ] 报告格式规范
|
||||
|
||||
---
|
||||
|
||||
## 第四阶段:可视化和交互
|
||||
|
||||
### 任务 4.1:分型面可视化增强
|
||||
|
||||
- [ ] 设置分型面颜色
|
||||
- [ ] 调整透明度
|
||||
- [ ] 边缘高亮
|
||||
- [ ] 与产品对比度
|
||||
|
||||
### 任务 4.2:分型线可视化增强
|
||||
|
||||
- [ ] 设置线条颜色
|
||||
- [ ] 调整线条粗细
|
||||
- [ ] 添加端点标记
|
||||
- [ ] 动态绘制效果
|
||||
|
||||
### 任务 4.3:交互式分型面调整
|
||||
|
||||
- [ ] 实现鼠标拖拽
|
||||
- [ ] 实时更新模型
|
||||
- [ ] 实现撤销功能
|
||||
- [ ] 实现重做功能
|
||||
|
||||
### 任务 4.4:交互式参数调整
|
||||
|
||||
- [ ] 实现滑块实时更新
|
||||
- [ ] 参数变化动画
|
||||
- [ ] 效果对比视图
|
||||
|
||||
### 任务 4.5:剖视图功能
|
||||
|
||||
- [ ] 实现剖切算法
|
||||
- [ ] 显示内部结构
|
||||
- [ ] 剖面切换动画
|
||||
- [ ] 多方向剖视
|
||||
|
||||
### 任务 4.6:测量工具
|
||||
|
||||
- [ ] 实现距离测量
|
||||
- [ ] 实现角度测量
|
||||
- [ ] 测量结果标注
|
||||
- [ ] 测量精度验证
|
||||
|
||||
### 任务 4.7:视角控制增强
|
||||
|
||||
- [ ] 添加预设视角
|
||||
- [ ] 实现动画过渡
|
||||
- [ ] 实现自动对准
|
||||
|
||||
### 任务 4.8:导出视图功能
|
||||
|
||||
- [ ] 实现 PNG 导出
|
||||
- [ ] 支持高清截图
|
||||
- [ ] 批量导出支持
|
||||
|
||||
---
|
||||
|
||||
## 第五阶段:数据接口和测试
|
||||
|
||||
### 任务 5.1:STEP 导出接口
|
||||
|
||||
- [ ] 实现 STEP 导出
|
||||
- [ ] 验证导出文件
|
||||
- [ ] 测试 CAD 兼容性
|
||||
|
||||
### 任务 5.2:JSON 数据导出
|
||||
|
||||
- [ ] 实现 JSON 导出
|
||||
- [ ] 包含完整参数
|
||||
- [ ] 包含几何数据
|
||||
- [ ] 包含质量报告
|
||||
|
||||
### 任务 5.3:PDF 报告导出
|
||||
|
||||
- [ ] 实现 PDF 生成
|
||||
- [ ] 添加质量报告内容
|
||||
- [ ] 添加图表
|
||||
- [ ] 格式规范美观
|
||||
|
||||
### 任务 5.4:IGES 格式支持
|
||||
|
||||
- [ ] 实现 IGES 导入
|
||||
- [ ] 实现 IGES 导出
|
||||
- [ ] 验证 CAM 兼容性
|
||||
|
||||
### 任务 5.5:集成测试
|
||||
|
||||
- [ ] 功能测试通过
|
||||
- [ ] 性能测试通过
|
||||
- [ ] 兼容性测试通过
|
||||
- [ ] 压力测试通过
|
||||
|
||||
### 任务 5.6:用户验收测试
|
||||
|
||||
- [ ] 功能演示完成
|
||||
- [ ] 用户反馈收集
|
||||
- [ ] 问题修复完成
|
||||
- [ ] 用户验收签字
|
||||
|
||||
---
|
||||
|
||||
## 功能验收检查表
|
||||
|
||||
### 核心功能
|
||||
|
||||
- [ ] 自动分模算法正常运行
|
||||
- [ ] 分型面检测准确
|
||||
- [ ] 分型线计算正确
|
||||
- [ ] 拔模角处理正确
|
||||
- [ ] 收缩补偿正确
|
||||
|
||||
### 参数系统
|
||||
|
||||
- [ ] 所有参数可配置
|
||||
- [ ] 参数验证正确
|
||||
- [ ] 参数模板可用
|
||||
- [ ] 参数保存成功
|
||||
|
||||
### 可视化
|
||||
|
||||
- [ ] 3D 模型正确显示
|
||||
- [ ] 分型面可视化
|
||||
- [ ] 分型线可视化
|
||||
- [ ] 型腔/型芯可视化
|
||||
- [ ] 交互操作流畅
|
||||
|
||||
### 质量检测
|
||||
|
||||
- [ ] 平滑度检测正常
|
||||
- [ ] 连续性检测正常
|
||||
- [ ] 结构检测正常
|
||||
- [ ] 可行性评估正常
|
||||
- [ ] 报告生成正常
|
||||
|
||||
### 数据接口
|
||||
|
||||
- [ ] STEP 导出正常
|
||||
- [ ] JSON 导出正常
|
||||
- [ ] PDF 导出正常
|
||||
- [ ] IGES 导出正常
|
||||
|
||||
---
|
||||
|
||||
## 性能验收检查表
|
||||
|
||||
- [ ] 分模时间 < 30 秒
|
||||
- [ ] 渲染帧率 > 30 FPS
|
||||
- [ ] 内存占用 < 2 GB
|
||||
- [ ] 支持 5 用户并发
|
||||
|
||||
---
|
||||
|
||||
## 代码质量检查表
|
||||
|
||||
- [ ] 代码符合 PEP 8 规范
|
||||
- [ ] 包含类型注解
|
||||
- [ ] 包含文档字符串
|
||||
- [ ] 单元测试覆盖
|
||||
- [ ] 无安全漏洞
|
||||
- [ ] 无硬编码密码
|
||||
|
||||
---
|
||||
|
||||
**文档结束**
|
||||
@@ -1,37 +0,0 @@
|
||||
# 交付物清单(本次产出)
|
||||
|
||||
## 1. 分析报告(Markdown)
|
||||
|
||||
- `docs/MOLD_ERP_ANALYSIS_REPORT.md`
|
||||
- `docs/ZERO_FINISHED_INVENTORY_CERTIFICATE.md`
|
||||
- `docs/PERFORMANCE_SCALABILITY_PLAN.md`
|
||||
- `docs/PERFORMANCE_BENCHMARKS.md`
|
||||
- `docs/UAT_CHECKLIST.md`
|
||||
- `docs/INTERFACE_INTEGRATION_CATALOG_TEMPLATE.md`
|
||||
- `docs/CONFLUENCE_ARCHIVE_STRUCTURE.md`
|
||||
|
||||
## 2. 数据库脚本(PostgreSQL)
|
||||
|
||||
- 冻结交付后模具订单(头+明细):`scripts/db/001_freeze_delivered_sales_orders.sql`
|
||||
- 约束与索引补齐:`scripts/db/002_indexes_and_constraints.sql`
|
||||
- 数据修复示例(状态归一):`scripts/db/003_data_fixups.sql`
|
||||
- 分区模板(按月):`scripts/db/010_partitioning_template.sql`
|
||||
- RLS 模板(按 org_id):`scripts/db/011_rls_template.sql`
|
||||
- 审计追溯(old/new + 操作者/IP/UA):`scripts/db/020_audit_trail.sql`
|
||||
- 审计留存清理(180 天):`scripts/db/021_audit_retention.sql`
|
||||
- 慢 SQL 采样(pg_stat_statements):`scripts/db/030_pg_stat_statements.sql`
|
||||
|
||||
## 3. 自动化测试(pytest)
|
||||
|
||||
- 交付后冻结:`tests/test_sales_order_delivered_freeze.py`
|
||||
- 订单/采购/库存主链路用例(参数化 ≥30):`tests/test_api_inventory_orders.py`
|
||||
|
||||
## 4. 代码加固点(已落地)
|
||||
|
||||
- 交付后冻结:应用层拒绝更新/删除/改状态/领料(`status=delivered`)。
|
||||
- 状态一致性:避免写入未允许的 `pending` 状态。
|
||||
- 性能优化:BOM 需求计算去 N+1;库存扣减使用原子更新降低并发超卖风险。
|
||||
|
||||
## 5. Word + PDF 导出建议
|
||||
|
||||
- 建议使用 pandoc 将 `docs/MOLD_ERP_ANALYSIS_REPORT.md` 导出为 docx/pdf,并将生成物作为 CI 产物归档。
|
||||
@@ -0,0 +1,197 @@
|
||||
# geMoldInsight 部署总览(DEPLOYMENT)
|
||||
|
||||
> 文档定位:**唯一的部署主题入口文档**。
|
||||
> 本文负责说明当前推荐部署模式、部署文档分工与历史文档去向;不承担全部 Linux 操作细节。详细 Linux 部署步骤见 [deployment/LINUX_SETUP.md](deployment/LINUX_SETUP.md),当前状态见 [STATUS.md](STATUS.md),架构边界见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前推荐部署模式
|
||||
|
||||
当前推荐模式为:
|
||||
|
||||
- **unified**:frontend + unified backend + moldinsight Celery worker
|
||||
|
||||
原因:
|
||||
- 适合本地开发与集成环境
|
||||
- 前端同域反代可以面对单一 backend
|
||||
- 比按路径把前端网关分流到两套后端更易维护
|
||||
|
||||
### 1.1 一键 Compose 部署(按文件名切换模式)
|
||||
|
||||
项目按"模式 ↔ Compose 文件"一一对应的方式支持一键部署。换文件名即可换模式:
|
||||
|
||||
| 模式 | Compose 文件 | 一键命令 |
|
||||
|---|---|---|
|
||||
| **unified**(默认) | [docker-compose.yml](../docker-compose.yml) | `docker compose up -d` |
|
||||
| moldinsight-only | [docker-compose.moldinsight.yml](../docker-compose.moldinsight.yml) | `docker compose -f docker-compose.moldinsight.yml up -d` |
|
||||
| inventory-only | [docker-compose.inventory.yml](../docker-compose.inventory.yml) | `docker compose -f docker-compose.inventory.yml up -d` |
|
||||
|
||||
> **模式切换唯一入口是 `-f` 文件名**。各 service 均未声明 `profiles`(compose 规则:声明了 profiles 的服务在不带 `--profile` 时不会被选中,裸 `up` 会报 `no service selected`);历史 `--profile full/moldinsight/inventory` 写法随本次拆分失效,请统一改用上表命令。
|
||||
|
||||
### 1.2 宿主机端口约定(默认 = 选择 A)
|
||||
|
||||
部署约定:**unified 模式下前端独占宿主端口,backend 不暴露宿主端口**——浏览器始终只面对一个源,由前端 Nginx 同域反代到 backend,彻底消除 CORS。
|
||||
|
||||
```env
|
||||
# .env(unified 模式最小集)
|
||||
FRONTEND_PORT=10003 # 浏览器入口;前端 Nginx 容器监听 8000,反代 /api 到 backend:8000
|
||||
# BACKEND_PORT 留空或不设 → backend 仅在 docker 网络 gemold_network 内被前端反代访问
|
||||
```
|
||||
|
||||
端口链路:
|
||||
|
||||
```
|
||||
浏览器 → http://宿主机:10003 → frontend容器:8000 → /api/* → backend容器:8000
|
||||
(宿主机 10003) (docker 网络内)
|
||||
```
|
||||
|
||||
何时选 B(前后端都暴露宿主端口):临时直连后端调试、压测、k8s 健康检查等特殊场景。设 `BACKEND_PORT=10005`(避开 10003)后重启 compose 即可——**不建议在常规生产部署中使用**,会引入 CORS 与攻击面问题。
|
||||
|
||||
宿主机端口映射由 `.env` 强制配置,compose 无默认值兜底(缺配置时启动期 fail-fast)。详见 [.env.example §端口配置](../.env.example)、[docs/deployment/PORT_CONFIG.md](deployment/PORT_CONFIG.md)。
|
||||
|
||||
### 1.3 镜像构建
|
||||
|
||||
首次部署或更新代码后先构建,再 `up`:
|
||||
|
||||
```bash
|
||||
bash deploy/build.sh # 显式构建 base / backend / frontend 3 个镜像(celery 复用 backend)
|
||||
```
|
||||
|
||||
或让 compose 构建:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
> **注意**:`docker compose up -d` 在本地已有同名镜像(如 `gemold-backend:latest`)时**不会自动重建**,会直接复用旧镜像启动。更新代码或 Dockerfile 后,必须 `docker compose build` 或 `docker compose up -d --build` 才会生效。
|
||||
|
||||
详细 Linux 部署步骤:
|
||||
- [deployment/LINUX_SETUP.md](deployment/LINUX_SETUP.md)
|
||||
|
||||
---
|
||||
|
||||
## 2. 支持的部署模式
|
||||
|
||||
### 2.1 unified
|
||||
|
||||
一个统一后端同时挂载 moldinsight + inventory。
|
||||
|
||||
适合:
|
||||
- 本地开发
|
||||
- 测试/集成环境
|
||||
- 小团队统一部署
|
||||
|
||||
**Compose 文件**:[docker-compose.yml](../docker-compose.yml)(**默认入口**)
|
||||
|
||||
**一键命令**:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
包含服务:`frontend` + `backend`(unified 入口)+ `moldinsight-celery`。
|
||||
|
||||
### 2.2 moldinsight-only
|
||||
|
||||
只部署模具分析后端。
|
||||
|
||||
适合:
|
||||
- 独立开放分析能力
|
||||
- 异步任务与文件处理独立扩容
|
||||
|
||||
**Compose 文件**:[docker-compose.moldinsight.yml](../docker-compose.moldinsight.yml)
|
||||
|
||||
**一键命令**:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.moldinsight.yml up -d
|
||||
```
|
||||
|
||||
包含服务:`moldinsight`(独立 API)+ `moldinsight-celery`(异步 worker)。
|
||||
|
||||
### 2.3 inventory-only
|
||||
|
||||
只部署进销存后端。
|
||||
|
||||
适合:
|
||||
- 独立部署 ERP / 库存能力
|
||||
- 与 moldinsight 分开发布节奏
|
||||
|
||||
**Compose 文件**:[docker-compose.inventory.yml](../docker-compose.inventory.yml)
|
||||
|
||||
**一键命令**:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.inventory.yml up -d
|
||||
```
|
||||
|
||||
包含服务:仅 `inventory`。不挂任何命名卷(inventory 无文件分析链路),不依赖 RustFS / MinIO。
|
||||
|
||||
部署模式的结构含义见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 部署文档分工
|
||||
|
||||
### 3.1 当前权威文档
|
||||
|
||||
- [DEPLOYMENT.md](DEPLOYMENT.md)
|
||||
- 部署入口与文档导航
|
||||
- [deployment/LINUX_SETUP.md](deployment/LINUX_SETUP.md)
|
||||
- Linux 环境下的详细部署操作说明
|
||||
|
||||
### 3.2 端口与配置说明
|
||||
|
||||
以下文档作为当前部署补充说明保留:
|
||||
- [deployment/DEPLOY_PORT.md](deployment/DEPLOY_PORT.md)
|
||||
- [deployment/PORT_CONFIG.md](deployment/PORT_CONFIG.md)
|
||||
|
||||
它们的职责分别是:
|
||||
- `DEPLOY_PORT.md`:端口暴露、端口规划与 Nginx / 防火墙层面的说明
|
||||
- `PORT_CONFIG.md`:环境变量、端口配置项与 direct run / compose 映射补充
|
||||
|
||||
它们是部署入口文档的补充参考,不替代本文或 [deployment/LINUX_SETUP.md](deployment/LINUX_SETUP.md)。
|
||||
|
||||
### 3.3 历史/阶段性部署材料
|
||||
|
||||
以下材料属于迁移期或历史说明,不应再视为当前部署权威:
|
||||
- [archive/PORT_REFACTOR_SUMMARY.md](archive/PORT_REFACTOR_SUMMARY.md)
|
||||
- [archive/FRONTEND_UNIFIED_DEPLOYMENT_PLAN.md](archive/FRONTEND_UNIFIED_DEPLOYMENT_PLAN.md)
|
||||
|
||||
这些材料已迁入 `docs/archive/`,仅用于历史追溯,不替代当前的 [DEPLOYMENT.md](DEPLOYMENT.md) 、 [deployment/LINUX_SETUP.md](deployment/LINUX_SETUP.md) 与 [deployment/DEPLOY_PORT.md](deployment/DEPLOY_PORT.md)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 当前部署事实
|
||||
|
||||
当前部署上的几个关键事实:
|
||||
|
||||
- 项目保持单仓库、单数据库
|
||||
- frontend 是独立前端工程
|
||||
- 后端支持模块化入口
|
||||
- moldinsight 的异步分析链路依赖 celery
|
||||
- PostgreSQL / Redis / 对象存储通常复用服务器已有服务,而不是必须由项目 compose 自带
|
||||
|
||||
这些事实的当前版本以 [STATUS.md](STATUS.md) 和 [deployment/LINUX_SETUP.md](deployment/LINUX_SETUP.md) 为准。
|
||||
|
||||
---
|
||||
|
||||
## 5. 相关专题文档
|
||||
|
||||
以下文档可作为部署/存储方向的补充参考,但不替代部署入口文档:
|
||||
|
||||
- [deployment/DEPLOY_PORT.md](deployment/DEPLOY_PORT.md)
|
||||
- [deployment/PORT_CONFIG.md](deployment/PORT_CONFIG.md)
|
||||
- [topics/storage/RUSTFS_STORAGE.md](topics/storage/RUSTFS_STORAGE.md)
|
||||
- [topics/storage/STORAGE_SETUP.md](topics/storage/STORAGE_SETUP.md)
|
||||
|
||||
---
|
||||
|
||||
## 6. 后续整理原则
|
||||
|
||||
部署文档后续将遵循以下规则:
|
||||
|
||||
- 部署入口信息只在本文维护
|
||||
- 操作步骤只在 [deployment/LINUX_SETUP.md](deployment/LINUX_SETUP.md) 维护
|
||||
- 历史迁移说明与阶段计划迁入 `docs/archive/`
|
||||
- README 只保留最短启动说明,不再承担部署手册职责
|
||||
@@ -1,162 +0,0 @@
|
||||
# geMoldInsight 演进路线图
|
||||
|
||||
> 本文档是代码与功能演进的执行清单,基于 2026-07-13 的全量代码体检。每项含「现象 / 证据 / 修法 / 验证」,按 P0→P3 推进,完成后勾选。
|
||||
|
||||
## 诊断
|
||||
|
||||
代码已演进到「双应用模块化」形态(`entrypoints/` + `moldinsight/` + `inventory/` + `shared/`),但有 **三处结构性缺失** 让快速迭代变贵,外加 **一批静默 bug** 正在让功能"看起来在跑其实没跑":
|
||||
|
||||
- **缺中间层**:业务逻辑堆在路由/编排函数里(进销存无 service 层、`process_file_core` 280 行线性函数)
|
||||
- **缺契约**:前后端靠手写类型,字段已大面积漂移(财务页整页是 0)
|
||||
- **缺连接**:模具分析与进销存是两个孤立产品(`STPFile` 没有 `product_id`)
|
||||
|
||||
---
|
||||
|
||||
## P0 止血:正在静默失效的功能(1–2 周)
|
||||
|
||||
这些不是技术债,是**现在就在坏**的东西,先修。
|
||||
|
||||
### P0-1 Celery worker 不连 Redis/RustFS,异步任务全坏
|
||||
- **现象**:任务进度写进 Celery 私有内存,web 端永远读不到;首次上传 RustFS 直接抛 `RuntimeError("RustFS 未连接")`。
|
||||
- **证据**:`redis_task_manager.connect()` / `rustfs_manager.connect()` 只在 FastAPI startup 调用(`entrypoints/moldinsight.py:42,48`),Celery 进程不跑 startup;`processing_service.py` 在 celery 内调 `update_task` 时 `is_connected=False` 走 `_fallback_set`;`rustfs_storage.py:125-126` 未连接直接抛错。`deploy/docker-compose.yml` 的 `moldinsight-celery` 服务块缺 `REDIS_PASSWORD`。
|
||||
- **修法**:`celery_tasks.py` 加 `@worker_process_init` 信号,显式 `connect()` redis 与 rustfs;补齐 celery 服务的 `REDIS_PASSWORD`/`SECRET_KEY` 等环境变量,与主应用对齐。
|
||||
- **验证**:上传一个 STP,Celery 路径下任务进度能从 web 端 `/api/status/{task_id}` 读到;上传后 RustFS 中能看到对象。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P0-2 LLM 设计报告 NameError,静默失效
|
||||
- **现象**:`LLM_ENABLED=true` 时设计报告功能直接没有。
|
||||
- **证据**:`llm_service.py:339` 用未定义变量 `trimmed`(应为 `features`,`trimmed` 只在 `_build_side_action_prompt` 中定义),外层 `try/except` 吞掉 `NameError` 返回 `None`。
|
||||
- **修法**:`trimmed` -> `features`。
|
||||
- **验证**:启用 LLM 后设计报告字段非空。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P0-3 前端财务页全字段错配
|
||||
- **现象**:FinanceTab 整页 0/空;用户管理菜单永不显示(`is_superuser` 后端不返回);dashboard 成品数恒 0。
|
||||
- **证据**:16 处字段名对不上,如 `total_receivable` vs `receivable_total`(`finance_schemas.py:66`)、`order_no` vs `txn_no`(`finance_schemas.py:48`)等;`App.vue:91` 读 `is_superuser` 但 `UserResponse` 无此字段。
|
||||
- **修法**:短期按映射手改前端字段;长期靠 P1-3 OpenAPI 契约生成根治。
|
||||
- **验证**:财务页卡片与表格显示真实数据;用户管理菜单对管理员可见。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P0-4 OCC 线程安全自相矛盾
|
||||
- **现象**:偶发崩溃,外层 `max_workers=1` 保护形同虚设。
|
||||
- **证据**:`processing_service.py:50-51` 用单线程池序列化 OCC,但 `geometry_analyzer._detect_features`(`geometry_analyzer.py:81`)内部又开 `ThreadPoolExecutor(max_workers=4)` 并行操作 OCC `TopoDS_Shape`。
|
||||
- **修法**:特征检测器改串行;或预处理阶段把面特征抽成纯数值,检测器只处理数值不碰 OCC。
|
||||
- **验证**:压测大模型反复分析无崩溃。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P0-5 `.env` 进了 git 历史,真实密钥泄露
|
||||
- **现象**:DB/Redis/SECRET_KEY/LLM key 已进入仓库历史。
|
||||
- **证据**:`git ls-files --error-unmatch .env` 命中;`git log -- .env` 有 10+ 次提交;`.env` 内含真实凭据。
|
||||
- **修法**:`git rm --cached .env`(停止跟踪,保留本地,后续不再提交);`SECRET_KEY` 从默认占位符轮换为强随机值。
|
||||
- **用户决策(2026-07-13)**:私有仓库,不轮换其他密钥、不重写 git 历史。
|
||||
- **验证**:`git status` 显示 `.env` 不再被跟踪(`D .env`)。
|
||||
- **状态**:- [x]
|
||||
|
||||
### P0-6 铝价路由模块化部署后丢失
|
||||
- **现象**:模块化部署后 `/api/aluminum-price/*` 直接 404。
|
||||
- **证据**:单体 `main.py:47,155` 挂了 `aluminum_price_router`,但 `moldinsight/api/__init__.py` 的 `_safe_include` 列表不含 `aluminum_price_routes`。
|
||||
- **修法**:把 `aluminum_price_routes` 加入 `_safe_include`。
|
||||
- **验证**:模块化部署下 `/api/aluminum-price/*` 可访问。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P0-7 导出缓存无持久化回退
|
||||
- **现象**:多 worker 或重启后导出返回 409。
|
||||
- **证据**:`processing_service._export_shapes_cache` 是进程内 dict,`get_export_shapes` 只查内存;`_persist_step_exports` 已写磁盘 manifest 但无回读逻辑。
|
||||
- **修法**:`get_export_shapes` 缓存未命中时从磁盘 manifest 回读。
|
||||
- **验证**:重启后导出仍可用。
|
||||
- **状态**:- [ ]
|
||||
|
||||
---
|
||||
|
||||
### P0 执行结果(2026-07-13)
|
||||
|
||||
- ✅ **P0-1 Celery 连接**:`celery_tasks.py` 在任务内显式 `redis_task_manager.reconnect()` + `rustfs_manager.connect()`(Redis 客户端绑定事件循环,每任务 reconnect;RustFS 同步客户端连一次复用);`deploy/docker-compose.yml` celery 服务补 `REDIS_PASSWORD`/`RUSTFS_TIMEOUT`
|
||||
- ✅ **P0-2 LLM NameError**:`llm_service.py:339` `trimmed` -> `features`
|
||||
- ✅ **P0-3 前端字段错配**:FinanceTab 全字段对齐 schema(summary/statement/product-statement/transaction 共 16 处);`UserResponse` 加 `is_superuser` + 统一 `_build_user_response` 构造(修用户管理菜单不显示);DashboardTab `product_count`->`finished_product_count`;PurchaseOrdersTab `received_at/paid_at`->`received_date/paid_date`;后端 `FinanceTransactionResponse` 补 `partner_name` 并批量查询客户/供应商名称
|
||||
- ✅ **P0-4 OCC 线程安全**:`geometry_analyzer._detect_features` `max_workers` 4->1
|
||||
- ✅ **P0-5 .env 泄露**:`git rm --cached .env` 已取消跟踪(后续不再提交);`SECRET_KEY` 从默认占位符轮换为强随机值(现有登录 token 失效)。用户决策:私有仓库,不轮换其他密钥、不重写 git 历史
|
||||
- ✅ **P0-6 铝价路由**:`moldinsight/api/__init__.py` `_safe_include` 加入 `aluminum_price_routes`
|
||||
- ℹ️ **P0-7 导出缓存**:经排查**非 bug**——`export_artifacts` 已写 PG+Redis(`processing_service.py:342,361`),导出端点先走 `_select_persisted_files` 从 task_data 读取(`advanced_router.py:436`),重启后正常工作;409 仅在持久化也失败时出现,"请重新分析"提示为正确行为。内存 re-export 缓存的可靠性优化归入 P1-2
|
||||
|
||||
**未做验证**:前端未跑 vue-tsc 构建(字段重命名属机械改动,低风险);后端未跑 pytest(需 DB/Redis 环境)。建议下次在完整环境验证。
|
||||
|
||||
---
|
||||
|
||||
## P1 结构性地基:让后续迭代不再昂贵(持续)
|
||||
|
||||
### P1-1 进销存抽 service 层
|
||||
- **现状**:`finance_routes.py` 751 行、`sales_order_routes.py` 777 行,事务编排/库存原子更新/流水写入全耦合在 endpoint;`shared/services/` 仅 auth+redis。
|
||||
- **目标**:新建 `inventory/services/`,`PurchaseOrderService.receive()`、`SalesOrderService.issue_materials()`、`FinanceService.settle()`,route 只做校验+组装。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P1-2 moldinsight 可插拔注册表 + Stage 流水线
|
||||
- **现状**:模具类型硬编码 if-else(`multi_scheme_planner.py:40`);特征检测器硬编码 6 个(`geometry_analyzer.py:76-99`);`process_file_core` 280 行。
|
||||
- **目标**:`FeatureDetectorRegistry` + `MoldGeneratorRegistry`(`@register` 装饰器);`process_file_core` 拆成 Stage 链。
|
||||
- **解锁**:新增模具类型、IGES/BREP、批量分析。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P1-3 前端 OpenAPI 契约生成
|
||||
- **现状**:前端 40+ 处 `any`,字段全手写已大面积错配。
|
||||
- **目标**:`openapi-typescript` 从 `/openapi.json` 生成 TS 类型替换 `any`;`api.ts` 加 baseURL/拦截器/超时,按域封装 `inventoryApi`/`moldinsightApi`/`authApi`。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P1-4 引入 Alembic,废除裸 DDL
|
||||
- **现状**:无 `alembic.ini`;`init_db.py` 22 条 `ALTER TABLE ADD COLUMN IF NOT EXISTS`,无版本/无回滚;`migrate_db.py` 是 `drop_all` 破坏性脚本;两应用 startup 并发跑 DDL 争锁。
|
||||
- **目标**:`alembic init`,固化版本化迁移,启动只 `upgrade head`;删 `migrate_db.py`。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P1-5 统一材料属性源
|
||||
- **现状**:材料字典在 4 处重复定义且冲突(PE 收缩率 `material_service` 0.020 vs `aluminum_foam_mold.py:92` 0.025)。
|
||||
- **目标**:`MaterialService` 作为唯一源,其他模块查询。
|
||||
- **状态**:- [ ]
|
||||
|
||||
---
|
||||
|
||||
### P1 执行结果(进行中)
|
||||
|
||||
- 🚧 **P1-1 进销存抽 service 层**(进行中):
|
||||
- ✅ 建立 `inventory/services/` 层,抽出 `FinanceService`(`finance_routes` 767→123 行,业务逻辑全下沉;路由仅做参数校验+响应组装)
|
||||
- ✅ 将 `schemas/` 与 `utils.py` 从 `inventory/api/` 移至 `inventory/` 顶层,打破 service↔api 循环导入(schemas 是共享 DTO、utils 是纯函数,本不应嵌在 api 层;这是正确分层)
|
||||
- ✅ import 测试通过:9 finance 路由 + 55 inventory 路由全部正常加载
|
||||
- ⏳ 待办:抽 `sales_order_service` / `purchase_order_service` / `inventory`+`stock_movement` service;清理死代码 `api/utils.py`(待 `git rm`)
|
||||
|
||||
---
|
||||
|
||||
## P2 功能演进:把两个产品变成一个
|
||||
|
||||
### P2-1 打通模具分析 -> 进销存(最高产品价值)
|
||||
- **现状**:`STPFile` 无 `product_id`,moldinsight 与 inventory 零数据关联。
|
||||
- **目标**:`STPFile` 加 `product_id` 外键(可空),分析完成后一键创建 `Product(finished)` 并回写。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P2-2 真 AI 落地,砍掉假 AI
|
||||
- **现状**:`ai_mold_assistant.py` 209 行纯 stub 从未被调用;`ai_parting_detector.py` GNN 框架完整但无权重;`llm_service` 是唯一真接 AI(且有 P0-2 bug)。
|
||||
- **目标**:聚焦一个能跑通的 AI 能力(LLM 扩到成本估算/工艺对话);GNN 要么真训权重,要么移除 stub。
|
||||
- **状态**:- [ ]
|
||||
|
||||
### P2-3 模具成本估算 + 批量分析
|
||||
- 依赖 P1-2 完成后才有性价比。
|
||||
- **状态**:- [ ]
|
||||
|
||||
---
|
||||
|
||||
## P3 工程治理(穿插顺手做)
|
||||
|
||||
- [ ] `create_app()` 工厂消除两入口重复引导,废弃单体 `main.py`
|
||||
- [ ] 删死依赖/死代码:Kafka(零引用)、`templates/` legacy、`ai_mold_assistant` stub、`ProcessingService.__init__` 3 个死实例
|
||||
- [ ] `get_db_session` 统一事务边界(commit/rollback),废除路由手动 commit
|
||||
- [ ] 连接池治理(3 进程峰值 150 > PG 默认 100),考虑 PgBouncer
|
||||
- [ ] 进销存 state 从模块级单例迁回 Pinia,tab 改子路由
|
||||
- [ ] 统一 `/health` 响应 schema;SPA fallback 排除 `/api` 前缀避免吞 404
|
||||
- [ ] CORS 收敛(`allow_origins=["*"]` + `allow_credentials=True` 不安全)
|
||||
|
||||
---
|
||||
|
||||
## 执行进度
|
||||
|
||||
| 阶段 | 项数 | 已完成 | 进行中 |
|
||||
|------|------|--------|--------|
|
||||
| P0 | 7 | 6 修复 + 1 排查 | - |
|
||||
| P1 | 5 | 0 | P1-1 进行中(finance 已抽) |
|
||||
| P2 | 3 | 0 | - |
|
||||
| P3 | 7 | 0 | - |
|
||||
@@ -0,0 +1,116 @@
|
||||
# 配置与运行(OPERATIONS)
|
||||
|
||||
> 文档定位:**配置 / 启动 / 环境 / 运维硬性要求的唯一归属**。
|
||||
> 部署入口与部署文档分工见 [DEPLOYMENT.md](DEPLOYMENT.md),Linux 详细步骤见 [deployment/LINUX_SETUP.md](deployment/LINUX_SETUP.md);当前状态见 [STATUS.md](STATUS.md),架构见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 配置来源与优先级
|
||||
|
||||
- 配置统一走**环境变量**,代码侧由 [src/shared/config/settings.py](../src/shared/config/settings.py) 的 `Settings` 单例经 `dotenv` + `os.getenv` 读取。
|
||||
- **本地运行**:仓库根 `.env`(`load_dotenv()` 自动加载;不在仓库内,参照 [.env.example](../.env.example) 复制编辑)。
|
||||
- **Compose 运行**:compose 文件用 `${VAR}` 从同目录 `.env` 注入容器环境变量;按模式对应不同文件名(见 [DEPLOYMENT.md §1.1](DEPLOYMENT.md))。
|
||||
- **键值约定**:
|
||||
- `DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD`:**惰性校验、无代码默认**——缺失时 import 不报错(便于测试/静态分析),真正连库时才失败。生产必须显式配置。
|
||||
- `AUTO_MIGRATE`:应用启动时是否自动执行 alembic 迁移,默认 `true`(单机开发语义);**多副本 / 容器编排部署应设 `false`**,改由部署流程单点执行 `alembic upgrade head` 或 `python -m shared.database.init_db`(迁移脚本已随镜像分发于 `/app/migrations/`)。
|
||||
- `SECRET_KEY`:JWT 签名密钥,**无默认**;生产必须 ≥32 字符强随机。
|
||||
- `ADMIN_PASSWORD`:初始管理员密码,**无默认**;首次建库前必须设置。
|
||||
- `RUSTFS_*`:对象存储(兼容 `MINIO_*` 别名写法);本地开发缺省值仅为占位,连不上会在用到存储的链路报错。
|
||||
- `REDIS_*`:默认 `localhost:6379` 无密码(本地开发语义),生产必须显式覆盖;连接串唯一拼装点为 `Settings.redis_url`(Celery broker/backend 复用)。
|
||||
- `MAX_FILE_SIZE`:上传文件大小上限(字节),默认 `104857600`(100MB);此前为死配置(处理器硬编码 50MB),2026-09-17 起真实生效,收紧上限需同步调整该值。
|
||||
- `CORS_ORIGINS`:逗号分隔白名单;不设默认放行 `*`,**生产必须显式设置**。
|
||||
- `LOG_FORMAT`:`json`(生产默认,结构化)/ `text`(开发人可读);`LOG_LEVEL`:DEBUG/INFO/WARNING/ERROR。
|
||||
- `DEBUG`:`true` 时额外注册 `/api/debug/*` 调试路由(仍需登录),**生产必须为 false**。
|
||||
- 新增配置项的规则:只加 `settings.py` + `.env.example`,关键依赖项不给 localhost/弱口令兜底(见 [AGENTS.md](../AGENTS.md) §2)。
|
||||
|
||||
## 2. 安装与环境
|
||||
|
||||
- 后端依赖:`pip install -r requirements.txt`。
|
||||
- **OCC 几何能力**:PythonOCC 不走 pip 主路径,通过 conda 环境提供(本项目实践环境名 `gemold` 或 `moldinsight`)。无 OCC 环境时项目可启动,但几何分析契约测试自动 skip。
|
||||
- 前端:`cd frontend && npm install`。
|
||||
- 数据库迁移:`migrations/`(`alembic.ini` 在仓库根;2026-09-16 由 `alembic/` 改名——原目录名与 alembic 包重名,应用内 import 会被遮蔽导致启动期迁移静默失败);数据修复类一次性脚本在 `scripts/migrations/` 与 `scripts/db/`,**不是运行时代码**,勿在服务内引用。
|
||||
|
||||
### 2.1 pip 锁文件生成(D13 流程)
|
||||
|
||||
`deploy/requirements-{base,moldinsight}.lock.txt` 是项目依赖的**版本锁**,由 conda 环境首次构建成功后一次性落盘:
|
||||
|
||||
- **生成时机**:在 `moldinsight` / `gemold` conda 环境(仅含项目依赖 + conda 基础库,**不能**在混装全开发栈的本机 pip 环境跑)执行 `pip freeze`
|
||||
- **生成命令**:
|
||||
- Linux / macOS:`bash deploy/generate_lockfiles.sh`
|
||||
- Windows:`deploy\generate_lockfiles.bat`
|
||||
- **产物**:
|
||||
- `deploy/requirements-base.lock.txt`
|
||||
- `deploy/requirements-moldinsight.lock.txt`
|
||||
- **消费方**:CI、离线构建、生产复现部署;`pip install -r deploy/requirements-base.lock.txt` 可直接锁定安装而不依赖 `>=` 解析
|
||||
- **提交策略**:两个 lock.txt 提交到仓库;版本下限(`requirements-{base,moldinsight}.txt`)按团队策略同步或保留 `>=` 灵活解析
|
||||
|
||||
## 3. 本地启动
|
||||
|
||||
后端三入口(均含 sys.path 修正,可从仓库根直接跑):
|
||||
|
||||
```bash
|
||||
# unified(moldinsight + inventory,推荐):8000
|
||||
uvicorn src.entrypoints.unified:app --reload --host 0.0.0.0 --port 8000
|
||||
# moldinsight-only:8000
|
||||
uvicorn src.entrypoints.moldinsight:app --reload --host 0.0.0.0 --port 8000
|
||||
# inventory-only:8001
|
||||
uvicorn src.entrypoints.inventory:app --reload --host 0.0.0.0 --port 8001
|
||||
```
|
||||
|
||||
Celery worker(moldinsight 异步分析链路;本地从 `src` 目录跑,与 compose 中 `moldinsight-celery` 的 `command:` 覆盖同参——worker 与后端共用 `gemold-backend` 镜像,无独立 Dockerfile):
|
||||
|
||||
```bash
|
||||
cd src && celery -A celery_app worker --concurrency=2 --loglevel=info
|
||||
```
|
||||
|
||||
- `--concurrency=N` 即 OCC 并行分析数:每个 prefork 子进程持一个常驻 OCC 工作进程(方案 B,见 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md))——每个 OCC 工作进程是独立的 Python + OCC 运行时,**N 增大时按「worker 子进程 + OCC 子进程」双份预算内存**,并预留 PG 连接数(按 celery 角色池随子进程倍增)。
|
||||
- `--max-tasks-per-child=M`(如 50):worker 子进程定期重启,连带回收其 OCC 子进程(进程级兜底,方案 A)。
|
||||
|
||||
前端:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev # Vite 开发服务器
|
||||
npm run gen:api # 从根目录 openapi.json 重新生成 src/types/api.ts(见 API_CONTRACT §4)
|
||||
```
|
||||
|
||||
探活:unified/moldinsight `GET /health` 与 `GET /api/health`;inventory `GET /health`。
|
||||
|
||||
## 4. Docker Compose
|
||||
|
||||
按"模式 ↔ 文件名"一一对应:
|
||||
|
||||
```bash
|
||||
# unified(默认;frontend + backend + moldinsight-celery)
|
||||
docker compose up -d
|
||||
|
||||
# moldinsight-only(moldinsight + moldinsight-celery)
|
||||
docker compose -f docker-compose.moldinsight.yml up -d
|
||||
|
||||
# inventory-only(仅 inventory)
|
||||
docker compose -f docker-compose.inventory.yml up -d
|
||||
```
|
||||
|
||||
> 旧 `--profile` 写法已失效(服务不再声明 profiles);模式切换唯一入口是 `-f` 文件名。
|
||||
|
||||
- 镜像构建:`bash deploy/build.sh`(base → backend → frontend 3 个 tag,celery 复用 backend 镜像);首次部署或更新代码后必须先 build(或 `docker compose up -d --build`)——裸 `up` 对本地已有同名镜像**不会自动重建**。
|
||||
- PostgreSQL / Redis / RustFS 通常**复用服务器已有服务**,不由项目 compose 自带;容器只注入连接配置。
|
||||
|
||||
## 5. 运行时硬性要求
|
||||
|
||||
- **生产环境必须显式设置**:`SECRET_KEY`、`ADMIN_PASSWORD`、`DB_*`、`CORS_ORIGINS`、`RUSTFS_*`、`REDIS_PASSWORD`、`DEBUG=false`、`LOG_FORMAT=json`。
|
||||
- **单数据库**:moldinsight 与 inventory 共享同一 PostgreSQL(刻意设计,不拆库)。
|
||||
- **后台任务一律走 `task_dispatcher`** 与 Celery,不要在路由里 fire-and-forget。
|
||||
- **uploads/ 与 html_output/ 为运行时产物目录**,不提交、不作为配置源头。`html_output/` 自 D11 起仅作 `/html` 报告代理的**存量兜底读**(新产物直传 RustFS 报告键 `html/reports/`,worker 不再写本地卷)。
|
||||
- `scripts/` 下的一次性脚本执行前先确认目标环境(多为不可逆数据迁移)。
|
||||
|
||||
## 6. 排障指针
|
||||
|
||||
| 症状 | 先看 |
|
||||
|---|---|
|
||||
| 起服务连不上数据库 | `.env` 的 `DB_*` 是否与服务器一致(惰性校验:import 成功 ≠ 连接正常) |
|
||||
| 上传/导出报对象存储错误 | `RUSTFS_*` 四项 + [topics/storage/RUSTFS_STORAGE.md](topics/storage/RUSTFS_STORAGE.md) |
|
||||
| 分析任务一直 pending | Celery worker 是否在跑;Redis 连通性;`task_dispatcher` 日志 |
|
||||
| 前端类型与接口对不上 | `openapi.json` 是否重新导出、`npm run gen:api` 是否执行([API_CONTRACT.md](API_CONTRACT.md) §4) |
|
||||
| 登录 401 | `SECRET_KEY` 是否跨进程一致(JWT 校验依赖同一密钥) |
|
||||
| 部署端口/反代问题 | [deployment/DEPLOY_PORT.md](deployment/DEPLOY_PORT.md)、[deployment/PORT_CONFIG.md](deployment/PORT_CONFIG.md) |
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
# geMoldInsight 演进路线图(ROADMAP)
|
||||
|
||||
> 文档定位:**未来演进路线与阶段计划的权威文档**。
|
||||
> 本文回答“下一步准备往哪里演进、按什么阶段推进”;不负责维护当前实现状态,当前状态见 [STATUS.md](STATUS.md)。架构边界见 [ARCHITECTURE.md](ARCHITECTURE.md),当前活跃技术债见 [TECH_DEBT.md](TECH_DEBT.md)。
|
||||
> 本文基于历史归档 [archive/EVOLUTION_ROADMAP.md](archive/EVOLUTION_ROADMAP.md) 收敛整理而来。
|
||||
|
||||
---
|
||||
|
||||
## 1. 演进背景
|
||||
|
||||
geMoldInsight 已从历史单体逐步演进为“双业务模块 + 共享平台层 + 独立前端”的结构,但要让后续迭代成本继续下降,仍需要在以下方向持续推进:
|
||||
|
||||
- 继续收敛模块边界
|
||||
- 继续减少 shared 的历史耦合
|
||||
- 让部署、文档、契约与代码结构保持一致
|
||||
- 让 moldinsight 与 inventory 的协作关系更稳定、可维护
|
||||
|
||||
当前事实与最近完成项见 [STATUS.md](STATUS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前演进主线
|
||||
|
||||
### 2.1 主线一:模块化架构收口
|
||||
|
||||
目标:
|
||||
|
||||
- 继续巩固 `moldinsight / inventory / frontend / shared` 的边界
|
||||
- 减少历史单体遗留语义
|
||||
- 让 README、架构文档、部署文档与代码结构一致
|
||||
|
||||
重点方向:
|
||||
|
||||
- 继续收敛 `shared` 的职责
|
||||
- 逐步明确 identity / platform 的边界语义
|
||||
- 收敛历史文档与旧部署叙事
|
||||
|
||||
### 2.2 主线二:moldinsight 工程化增强
|
||||
|
||||
目标:
|
||||
|
||||
- 让 STEP/STP 分析链路更稳定
|
||||
- 让导出、批量分析、成本估算、任务状态等链路更可靠
|
||||
- 继续提高 OCC 相关处理的可维护性与可测试性
|
||||
|
||||
重点方向:
|
||||
|
||||
- ~~`advanced_router` 拆分与请求模型规范化~~(2026-09-17 批次 3 完成)
|
||||
- ~~D17 Human-in-Loop 老师傅经验反馈~~(2026-09-23~24 完成,3 个 commit:数据 + 权限 + 写入 API / 算法接缝 + OCC payload / 前端按钮 + Dialog + 经验角标;写入即消费闭环通;详见 [TECH_DEBT.md](TECH_DEBT.md) D17)
|
||||
- 模具分析链路的结构继续收口
|
||||
- OCC 依赖场景下的契约测试/集成测试继续补齐
|
||||
|
||||
### 2.3 主线三:inventory 业务层继续沉淀
|
||||
|
||||
目标:
|
||||
|
||||
- 让 inventory 从“可用”继续走向“可扩展”
|
||||
- 继续将路由中的业务逻辑下沉为 service 层
|
||||
- 保持与 moldinsight 的桥接模型清晰
|
||||
|
||||
重点方向:
|
||||
|
||||
- 业务 service 复用强化
|
||||
- 数据模型归属进一步清晰化
|
||||
- 前后端契约持续减少手写漂移
|
||||
- 已完成第一批主数据收口(2026-09-21):`customer / supplier / warehouse` 路由改为薄路由,CRUD 编排下沉至 `master_data_service`
|
||||
- 已完成物料域第二批收口(2026-09-21):`material_routes` 的价格历史、价格趋势、供应商关联查询/删除编排下沉至 `material_service`
|
||||
- 已完成产品域第三批收口(2026-09-21):`product_routes` 的常规 CRUD、BOM 与跨模块 `from-task` 编排均已下沉至 `product_service`
|
||||
- 已完成 dashboard 聚合收口(2026-09-21):`dashboard_routes` 的首页统计/低库存预警编排下沉至 `dashboard_service`
|
||||
|
||||
### 2.4 主线四:部署与运维一致性
|
||||
|
||||
目标:
|
||||
|
||||
- 让推荐部署模式、Compose 入口、运维文档、Nginx/端口说明不再冲突
|
||||
- 让前端、后端、异步任务链路在部署说明上形成单一叙事
|
||||
|
||||
重点方向:
|
||||
|
||||
- 继续收口部署文档
|
||||
- 把历史部署迁移方案移入归档
|
||||
- 保持同域前端 + unified backend 的默认认知清晰
|
||||
|
||||
---
|
||||
|
||||
## 3. 下一阶段优先项
|
||||
|
||||
### P0:文档与边界对齐
|
||||
|
||||
- 建立 `STATUS / ARCHITECTURE / ROADMAP / TECH_DEBT / DEPLOYMENT` 主骨架
|
||||
- 将 README 收敛为唯一导航入口
|
||||
- 收口部署重复文档并建立 archive
|
||||
|
||||
### P1:moldinsight API 结构整理
|
||||
|
||||
- ~~拆分 `advanced_router`~~(2026-09-17 批次 3 完成)
|
||||
- ~~为高频接口引入 Pydantic 请求模型~~(2026-09-17 批次 3 完成)
|
||||
- 继续减少 `request.json()` 风格手动解析(存量端点已清零,新增接口守此约定)
|
||||
|
||||
### P2:shared/platform 边界继续收敛
|
||||
|
||||
- ~~梳理共享 ORM 与业务模型的归属~~(2026-09-17 批次 4 完成:ORM 已按模块拆分,跨模块只许裸 FK)
|
||||
- 继续减少 shared 直接承担业务组合逻辑
|
||||
- 为后续平台层命名与目录调整准备条件
|
||||
|
||||
### P3:专项能力继续规范化
|
||||
|
||||
- ~~铝价模拟数据增加显式 `source: "simulated"`~~(2026-09-18 完成:后端响应带 `source` 字段,前端按来源渲染标注,不再硬编码交易所名)
|
||||
- 补专题文档的定位/边界说明
|
||||
- 清理历史 checklist / tasks / report 文档的展示层级
|
||||
|
||||
---
|
||||
|
||||
### 3.1 后端设计治理批次(2026-09 设计审查产出)
|
||||
|
||||
> 2026-09-15 完成 moldinsight 后端设计审查,产出的具体治理批次是当前下一阶段最具体的执行计划。
|
||||
> 债务明细与逐项现状见 [TECH_DEBT.md](TECH_DEBT.md) §3(D5–D14);本小节只描述批次、顺序与每批归属。
|
||||
|
||||
| 批次 | 主题 | 内容 | 对应债务 |
|
||||
| ------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
|
||||
| 批次 0 | 安全与诚实(0.5–1 天) | `/api/status/{task_id}` 补鉴权 + 任务归属校验;`pythonocc_available` 真实检测;bcrypt 超长密码拒绝;SECRET_KEY / RUSTFS_* 惰性校验补齐 | D5 |
|
||||
| 批次 1 | 部署正确性(1–2 天) | 主链路改走 RustFS(分派入参`file_path` → `stp_file_id`,worker 按 object_key 下载解析);compose 共享卷兜底(过渡);alembic 移出 startup(`AUTO_MIGRATE` 开关);OCC 镜像引入方式修正 + 依赖锁文件 | D6、D12、D13 |
|
||||
| 批次 2 | 任务一致性模型(2–4 天) | PG 为单一事实源、Redis 仅热缓存;去掉多进程内存回退;批量元数据入库;型腔失败标 failed;持久化事务边界收口 | D7、D8、D9、D11 |
|
||||
| 批次 3 | API 与代码结构(3–5 天) | `_safe_include` 失败显式化(/health 暴露缺失路由);advanced_router 拆分 + Pydantic 请求模型;async 重计算统一 executor;StorageIntegrationService 拆分;配置治理 | D1、D14 |
|
||||
| 批次 4 | 架构演进(5 天+) | ~~共享 ORM 按模块拆分;OCC 吞吐方案设计先行;文档 / 契约同步~~(2026-09-17 完成) | D3、D10 |
|
||||
|
||||
**执行顺序建议**:批次 0 与批次 1 的 D6(RustFS 主链路)先行——前者是确认的安全漏洞,后者是部署根本性缺陷,两者互不依赖、改动可控。其余按批次顺序推进,每批完成同步 STATUS / TECH_DEBT / API_CONTRACT。
|
||||
|
||||
> 进度:批次 0 / 1 / 2 已于 2026-09-16 完成、批次 3 / 4 已于 2026-09-17 完成,§3.1 批次计划**全部执行完毕**;批次 4 后续专项于 2026-09-18 完成——D11(HTML 报告 RustFS 单源 + `/html` 代理路由)与 **OCC 方案 B(`run_occ` 契约进程化 + 常驻进程池 kill-on-timeout)已清偿**(部署参数方案 A 一并落地,见 [TECH_DEBT.md](TECH_DEBT.md) D10 与 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md))。遗留:D13 的 pip 全量锁文件随下次镜像构建补齐。完成明细见 [STATUS.md](STATUS.md) 与 [TECH_DEBT.md](TECH_DEBT.md) §2.5–2.8。后续优先项回到 §3 P2 / P3 与主线方向。
|
||||
|
||||
---
|
||||
|
||||
## 4. 中长期方向
|
||||
|
||||
### 4.1 平台层语义收敛
|
||||
|
||||
长期仍建议将 `shared` 逐步收敛为更清晰的平台层语义,但这应建立在:
|
||||
|
||||
- 当前模块边界稳定
|
||||
- 共享职责分层足够清晰
|
||||
- 文档与部署已经同步收口
|
||||
|
||||
### 4.2 文档体系持续治理
|
||||
|
||||
后续文档治理原则:
|
||||
|
||||
- README 只做入口
|
||||
- 当前状态只在 [STATUS.md](STATUS.md)
|
||||
- 历史材料统一入 `docs/archive/`
|
||||
- 每个主题只有一篇默认权威文档
|
||||
|
||||
### 4.3 测试能力继续增强
|
||||
|
||||
重点继续放在:
|
||||
|
||||
- OCC 相关集成验证
|
||||
- 跨模块关键链路回归测试
|
||||
- 关键契约的自动化保护
|
||||
|
||||
---
|
||||
|
||||
### 4.4 专题文档持续分级
|
||||
|
||||
后续还会继续把专题文档区分为三类:
|
||||
|
||||
- 当前仍有参考价值的专题文档(保留并补定位)
|
||||
- 纯阶段性任务/检查单/迁移计划(迁入 archive)
|
||||
- 可被主骨架吸收的重复说明(逐步收口)
|
||||
|
||||
---
|
||||
|
||||
## 5. 与相关文档的边界
|
||||
|
||||
- 当前项目处于什么状态:看 [STATUS.md](STATUS.md)
|
||||
- 当前架构与边界是什么:看 [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||
- 当前有哪些技术债:看 [TECH_DEBT.md](TECH_DEBT.md)
|
||||
- 当前部署方式怎么做:看 [DEPLOYMENT.md](DEPLOYMENT.md)
|
||||
- 更完整的模块化蓝图讨论:看 [archive/BACKEND_MODULARIZATION_BLUEPRINT.md](archive/BACKEND_MODULARIZATION_BLUEPRINT.md)
|
||||
@@ -0,0 +1,55 @@
|
||||
# geMoldInsight 项目状态(STATUS)
|
||||
|
||||
> 文档定位:**唯一的「现在到哪了」**。README / AGENTS / 各主文档只链接到这里,不复制状态内容。
|
||||
> 维护规则:每完整完成一个需求,**倒序在本文顶部加一条**(日期 + 主题 + 关键事实);其余主文档(架构 / 规划 / 技术债 / 部署)维护各自的"当前有效说法",本文只记录"什么时候做到了哪一步"。维护规则出处见根目录 [AGENTS.md](../AGENTS.md)。
|
||||
> 早期条目(2026-09-17 之前)已精简为锚点,完整流水见 [archive/2026-09_governance_batches.md](archive/2026-09_governance_batches.md) 与 [archive/2026-09_status_history.md](archive/2026-09_status_history.md)。
|
||||
|
||||
> 2026-09-27(**分模方案预览契约修复:关键工艺参数面板整片 N/A + 备选方案预览无法切换**——多方案重构后 `cavity_data` 与可视化端的字段契约断裂,两处收口。① **legacy 契约桥接**:新增 `CalculationService.attach_scheme_info_contract`([calculation_service.py](../src/moldinsight/services/calculation_service.py)),多方案规划器([multi_scheme_planner.py](../src/moldinsight/core/multi_scheme_planner.py) `_build_scheme`)生成 `cavity_data` + `key_info` 后立即把方案级 `key_info` 内嵌回 `cavity_data`(`mold_cavities.cavity_key_info` + `cavity_count` + `manufacturing_info.mold_material / mold_hardness / surface_finish / estimated_cycle_time / parting_line_length`)——3D 预览"关键工艺参数"面板([html_generator.py](../src/shared/utils/html_generator.py) `updateInfoPanel` / `updateSummaryPanels`)只拿得到 `cavity_data`,多方案重构后这些字段仅存在于 `scheme.key_info`,面板 10 项 8 项恒 N/A、前端结果页"型腔数"恒回退 1 腔(根因佐证:`_strip_heavy_geometry` 专门保留 `mold_cavities.cavity_key_info`,说明预览契约仍是 legacy 结构,多方案生成器未遵守);② **方案级预览恢复**:[processing_service.py](../src/moldinsight/services/processing_service.py) `_attach_scheme_previews` 由"仅最优方案生成完整 HTML + 其余方案只生成 summary JSON(前端零消费死产物 `summary_file`,已删除)"改为**每方案生成完整预览**(HTML + `_summary.json` + `_data.json` 三件直传 RustFS 报告键)并写 `scheme["html_file"] = /html/{name}`——前端 ResultView 预览取 `selectedScheme?.html_file`,此前后端从不写方案级 `html_file`,切方案 iframe `:key` 不变,备选方案永远显示推荐方案预览;任务级 HTML 复用推荐方案预览不再重复生成(`save_html_file` 要求本地文件存在,`_attach_scheme_previews` 返回推荐方案预览本地路径,无任何方案 `cavity_data` 时回退任务级单份生成);③ 前端 [ResultView.vue](../frontend/src/modules/moldinsight/ResultView.vue) 3D 预览标题栏加"当前:方案名" t-tag(多方案时渲染),切换有明确反馈。**已知取舍**:每方案预览复制一份网格数据 JSON,RustFS 报告键存储随方案数线性增长,换备选方案真实独立分模预览(存储吃紧时演进共享产品网格 + 方案差分,见 [TECH_DEBT.md](TECH_DEBT.md) D11 追记)。**历史任务不回填**:已入库任务缺字段与每方案预览文件,需重新分析才能看到完整参数与切换预览。**接口面零变化**(无路由/Pydantic schema 变更,`scheme.html_file` 为 `/api/status/{task_id}` 自由 dict 内新增字段,openapi.json 不触发重导出)。**测试基线**:**193 passed, 13 skipped**(净增 1:`test_calculation_service_bridges_scheme_info_contract`,OCC-free 直测契约桥接);前端 `vue-tsc -b` 通过。**说明**:本地 pip 环境无 pythonocc,`multi_scheme_planner` 运行时路径未实跑(py_compile 通过;core→services import 模式与 `aluminum_foam_mold` 引 `MaterialService` 同款,无循环依赖),OCC 全量验证待 conda 环境补跑。)
|
||||
>
|
||||
> 2026-09-26(**端口默认值统一 10003 / 10004**:约定"容器内部端口无所谓,重要的是映射到宿主机的端口;前端页面 = 10003"。① [.env.example](../.env.example) 端口段重写:移除冗余的 `HOST` / `PORT`(uvicorn 命令硬编码,未读取)+ 移除误导性的"应用内部监听端口"注释;新增端口段约定(`FRONTEND_PORT=10003` 浏览器入口、`BACKEND_PORT=10003` 同端口供调试直连、`MOLDINSIGHT_PORT=10003` / `INVENTORY_PORT=10004` 独立模式);② [docker-compose.yml](../docker-compose.yml) frontend 默认端口回退 `80→10003`、backend 默认 `8000→10003`、删除 backend service 内冗余的 `HOST/PORT` env(uvicorn `--host/--port` 已是单一事实源,env 无代码读);③ [docker-compose.moldinsight.yml](../docker-compose.moldinsight.yml) / [docker-compose.inventory.yml](../docker-compose.inventory.yml) `MOLDINSIGHT_PORT/INVENTORY_PORT` 默认 `8000/8001→10003/10004`;④ [docs/deployment/DEPLOY_PORT.md](../docs/deployment/DEPLOY_PORT.md) §3 / [docs/deployment/PORT_CONFIG.md](../docs/deployment/PORT_CONFIG.md) §1 §2 端口映射示例同步。**验证**:yaml 渲染后端口映射 `[unified] frontend 10003→80 / backend 10003→8000`、`[moldinsight] 10003→8000`、`[inventory] 10004→8001`,与约定一致。**遗留**:服务器 `.env` 与新版 `.env.example` 对齐(已有字段名一致,仅注释差异,不需要重设值)。
|
||||
>
|
||||
> 2026-09-26(**Compose 拆分部署机端到端复验:3 个收尾修复 + 1 处文档澄清**——① `--workdir` 误用修复:[docker-compose.yml](../docker-compose.yml) / [docker-compose.moldinsight.yml](../docker-compose.moldinsight.yml) 中 `moldinsight-celery` 的 `command:` 原照搬旧 Dockerfile.celery 的 `celery worker --workdir=/app/src ...`,celery 5.x 已移除 `--workdir` 选项(部署机实测报 `No such option '--workdir'`),改为 `cd /app/src && exec celery -A celery_app worker ...`——celery_app.py 内 `include=["celery_tasks"]` 为裸模块名,必须在 `src/` 下启动 worker,与是否支持 `--workdir` 解耦,跨 celery 版本稳定;② **裸 `up` 不重建已有镜像**澄清:部署机复用旧 gemold-backend 镜像起容器(旧 miniconda base + 旧代码),`docker compose up -d` 仅在本地无同名镜像时构建,DEPLOYMENT §1.2 / README / OPERATIONS §4 / LINUX_SETUP §11 同步补一句"更新代码后须 `up -d --build` 或先 `docker compose build`";③ build.sh 步骤由"四步(base→backend→celery→frontend)"修正为"三步(base→backend→frontend,celery 复用 backend 镜像)"——`Dockerfile.celery` 早已删除但 build.sh 与 README 的描述未跟改,三处文档统一收口。**遗留**:服务器 `docker compose up -d --build` 重建验证新 base + 新 celery 启动命令端到端可用。)
|
||||
>
|
||||
> 2026-09-24(**Compose 按部署模式拆分为三个一键文件 + 文档全量同步**:① 单文件 profile 编排拆为"模式 ↔ 文件名"一一对应的三文件——[docker-compose.yml](../docker-compose.yml)(unified 默认入口:frontend + backend + moldinsight-celery,`docker compose up -d` 即起)+ [docker-compose.moldinsight.yml](../docker-compose.moldinsight.yml)(moldinsight-only:独立 API + celery)+ [docker-compose.inventory.yml](../docker-compose.inventory.yml)(inventory-only:仅 inventory,不声明任何命名卷避免空卷);② **服务不再声明 `profiles`**——compose 规则是声明了 profiles 的服务在裸 `up` 下不会被选中(拆分首版保留 profiles 导致裸 `up` / 裸 `-f` 均报 `no service selected`,部署机实测暴露后移除),模式切换唯一入口是 `-f` 文件名,历史 `--profile full/moldinsight/inventory` 写法随拆分失效(其目标服务本就已移出默认文件,兼容无意义);③ **顺手修复两个既有部署隐患**——moldinsight-only 场景 celery 的 `depends_on` 悬空(原指向被 profile 过滤掉的 `backend`,现各文件内分别指向 `backend` / `moldinsight`),以及 `gemold-moldinsight:latest` 与 `gemold-backend:latest` 双 tag 漂移(moldinsight service 的 image 统一为 `gemold-backend:latest`,与 [Dockerfile.celery](../deploy/Dockerfile.celery) 的 FROM 对齐,干净环境单跑 moldinsight-only 不再构建失败);④ `gemold_network` / `uploads_data` / `html_data` 加 `name:` 固定命名,跨文件 / 跨模式可复用;每文件内部以 YAML anchor(`x-base-env`)收敛 35+ 行重复 environment,`SECRET_KEY` / `ADMIN_PASSWORD` 的 `${VAR:?}` fail-fast 校验保留;⑤ 文档同步 11 文件:[DEPLOYMENT.md](DEPLOYMENT.md) §1.1 新增一键部署总表 + §2 三模式各附文件名与一键命令,[deployment/LINUX_SETUP.md](deployment/LINUX_SETUP.md) §6/§11 重写,[README.md](../README.md) 快速开始与 Compose 入口、[OPERATIONS.md](OPERATIONS.md) §4、[deploy/build.sh](../deploy/build.sh) / [.bat](../deploy/build.bat) 末尾提示、PORT_CONFIG / DEPLOY_PORT / STORAGE_SETUP / frontend/README 链接全部对齐(`AGENTS.md` §4.1 部署方式→DEPLOYMENT 同步规则满足);⑥ **部署机首次实测再暴露并修复两个干净机器构建必挂点**——(a) [.dockerignore](../.dockerignore) 自"重写独立dockerfile"起排除整个 `deploy/`,而 Dockerfile.frontend 要 COPY `deploy/nginx/frontend.conf`、Dockerfile.moldinsight 要 COPY `deploy/requirements-*.txt`(历史一直有旧镜像兜底未暴露;BuildKit 不支持重包含被排除目录的子文件,直接移除该行,deploy/ 仅几 KB 无上下文负担);(b) `Dockerfile.celery` `FROM gemold-backend:latest` 在 compose 并行构建下引用尚不存在的本地镜像必挂——**删除 Dockerfile.celery**,`moldinsight-celery` 改为与 API 服务**同一 build 声明 + 同一 `gemold-backend:latest` tag**(compose 去重只构建一次),celery 仅以 `command:` 覆盖启动 worker(`--concurrency` / `--max-tasks-per-child` 参数经 compose 命令与 `.env` 透传,语义不变),build.sh/.bat 移除 gemold-celery 构建步骤,OPERATIONS / OCC_THROUGHPUT / TECH_DEBT / .env.example 的 Dockerfile.celery 指向同步改写;⑦ **镜像 base 由 miniconda 切换 Miniforge**——[Dockerfile.moldinsight](../deploy/Dockerfile.moldinsight) FROM `continuumio/miniconda3:24.7.1-0` → `condaforge/miniforge3:24.7.1-2`(conda-forge 默认且唯一渠道,无 defaults 渠道与 Anaconda ToS 顾虑;与 CI 已用的 Miniforge 安装、开发机 Miniforge 同源;tag 经 Docker Hub 社区用例确认存在;conda create 步骤与 python=3.12 / pythonocc-core=7.9.0 锁定不变),[TECH_DEBT.md](TECH_DEBT.md) D13 锁定记录同步。**验证**:三文件 YAML 解析 + 结构静态校验通过(services / depends_on / 卷声明 / anchor 合并 / 网络命名 / 无 profiles 残留 / celery 与 API 服务 build 声明一致性);5 个 service 的 environment 键与拆分前逐一比对(YAML 展开合并键后 39/39、34/34、39/39、34/34、20/20)零丢失。**遗留**:部署机 `git pull` 后裸 `docker compose up -d` 端到端复验;base 镜像(miniconda3 / node / nginx)拉取依赖 docker.io 连通性,不通时需配镜像加速。
|
||||
>
|
||||
> 2026-09-24(**D17 Human-in-Loop 老师傅经验反馈批 3 上线(前端按钮 + Dialog + 经验角标)——闭环可视**:① [ResultView.vue:35-47](frontend/src/modules/moldinsight/ResultView.vue#L35-L47) 方案卡片 summary-header 加 `t-tag theme="success" variant="light"` 经验角标("📚 历史经验 N 条"),从 `hintsByAxis[currentAxisHint]` 读取,按 scheme_axis 索引,无 hints 时不渲染;② [ResultView.vue:131-138](frontend/src/modules/moldinsight/ResultView.vue#L131-L138) `export-buttons-bar` 加 `👍 老师傅反馈` 按钮(`v-if="canGiveFeedback"` 角色门控:admin 或 process_engineer);③ 新建 [components/HumanFeedbackDialog.vue](frontend/src/modules/moldinsight/components/HumanFeedbackDialog.vue):t-dialog + t-form + t-radio-group 三选一(采纳 / 建议调整 / 拒绝)+ t-textarea 原因 + 调整建议(仅 adjust 模式显隐);提交走 `moldinsightApi.submitExperienceFeedback`,成功后 emit `submitted` 让父组件重拉 hints 刷新角标;④ [shared/api-client.ts:407-444](frontend/src/shared/api-client.ts#L407-L444) `moldinsightApi` 新增 `getExperienceHints` / `submitExperienceFeedback` 两个方法(生成类型由 openapi-typescript 自动产出);⑤ [ResultView.vue](frontend/src/modules/moldinsight/ResultView.vue) `onMounted` 调 `loadExperienceHints` 拉一次 + 反馈提交后 `onFeedbackSubmitted` 再拉一次(写入即消费前端可见);`canGiveFeedback` 走 `is_superuser || roles 含 process_engineer` 表达式(项目硬规则"前端不要破坏 ResultView.vue 视觉一致性":按钮与既有 6 个 t-button 同一 `export-buttons-bar`,theme/size 一致;角标 theme="success" variant="light" 与既有 t-tag theme="primary" / "warning" 同款)。**接口变更三件套随批完成**:openapi.json 重导出(2 个新 path,含 ExperienceFeedbackCreate / ExperienceHintItem / ExperienceHintsResponse 三个新 schema)→ `npm run gen:api` 再生 types/api.ts → `npm run build` 通过。**后端基线**:**192 passed, 13 skipped**(批 3 不改后端);**前端构建**:vue-tsc + vite 通过,ResultView 包大小 37.18 kB / 11.91 kB gzip。**D17 闭环端到端可用**:admin / process_engineer 在 ResultView 点"👍 老师傅反馈" → Dialog 选"采纳"+ 写原因 → 提交 → 角标即时刷新(重拉 hints);下次同指纹 STP 分析,`PartingCandidateGenerator` 候选方向加成 + `PartingSchemeScorer` total_score 加成 + method 标签升级 `human_experience_primary`。**下一步**:批 4(衰减机制完善 + DFM 规则库独立模块化 + 经验冲突仲裁 UI)按需排期。)
|
||||
|
||||
> 2026-09-23(**D17 Human-in-Loop 老师傅经验反馈批 2 上线(算法接缝 + OCC payload 通道)——闭环通**:① 算法层 4 个核心文件加 `hints` 形参透传链:[parting_candidate_generator.py:13-66](src/moldinsight/core/parting_candidate_generator.py#L13-L66) `_build_axis_metrics` 末尾按 hints 加成(`weight × 20` 上限,`sample_count ≥ 2 + weight ≥ 0.5` → method 标签升级 `human_experience_primary`);[parting_scheme_scorer.py:8-46](src/moldinsight/core/parting_scheme_scorer.py#L8-L46) `_score_scheme` 新增 `human_hint_bonus` 字段(weight × 12 上限,sample_count < 2 时 ×0.5 折半),纳入 total_score;[multi_scheme_planner.py:26-86](src/moldinsight/core/multi_scheme_planner.py#L26-L86) `generate_plan` 透传 hints 到下两层,`global_summary.applied_hints` 注入返回;② [processing_service.py:531-595](src/moldinsight/services/processing_service.py#L531-L595) `_step_generate_cavity` 调 `experience_feedback_service.resolve_for_process_params` 拿同指纹 hints,装进 run_occ payload 顶层 `experience_hints` 字段(普通 dict 透传,pickle 安全,满足 [occ_worker.py:7-8](src/moldinsight/core/occ_worker.py#L7-L8) 硬规则);③ [occ_worker.py:117-140](src/moldinsight/core/occ_worker.py#L117-L140) `_op_generate_cavity` 读 `payload.get("experience_hints") or {}` 透传给 `planner.generate_plan(..., hints=...)`;④ D17 闭环验证:老师傅写一条同指纹 `adopted` → 同 X 通道下次分析 `priority_score` +18,`score_breakdown.human_hint_bonus` +12(sample_count=3),method 标签升级 `human_experience_primary`。**接口面零变化**(路径 / schema 不动;仅 OCC 子进程内部响应含 `global_summary.applied_hints`,由前端 ResultView 渲染角标——批 3 实现)。**测试基线**:**192 passed, 13 skipped**(批 2 净增 7 通过 + 4 OCC-gated skip:candidate_generator 3 例 / scheme_scorer 4 例在无 OCC 环境跑通,multi_scheme_planner + processing_service 4 例 OCC-gated 待 conda `gemold` 镜像验证)。**接口变更三件套执行节点**:openapi.json 重导出与前端 `gen:api` 待批 3 完成后一并执行(前端调用两 path + ResultView 渲染一并改)。**下一步**:批 3 前端(ResultView 按钮组 + `HumanFeedbackDialog.vue` + `moldinsightApi` 两个方法 + 经验角标)。)
|
||||
|
||||
> 2026-09-23(**D17 Human-in-Loop 老师傅经验反馈批 1 上线(数据 + 权限 + 写入 API)**:① 新增 `experience_feedback` 表(32 表迁移,alembic head `b7d1f4a92c3e`)——老师傅对系统推荐方案给出"采纳 / 调整 / 拒绝"反馈,按"产品指纹 + 工艺参数"为键跨任务匹配,下次同指纹产品分析自动消费;② 新增 3 个权限码(`view_experience_feedback` / `feedback_experience_hint` / `manage_experience_feedback`)+ 新角色 `process_engineer`(含 view + feedback 权限,admin 角色 permissions 同步补齐);③ 新增 2 个端点(`POST /api/tasks/{task_id}/experience-feedback` 提交反馈 + `GET /api/tasks/{task_id}/experience-hints` 拉取同指纹历史 hints 摘要);④ `init_db.py` 幂等 bug 修复——既有 DB 启动期不再跳过新增权限 / 角色补登(`init_permissions` / `init_roles` 改为按 code 比对,新增保留已有 id);⑤ ORM / 迁移 / service / router / api 注册均落位:D9 边界(service.flush + 路由 commit);D17 衰减(写新反馈时同 `stp_file_id` 整体续期 90 天 TTL);`User.has_permission` 全仓首次调用点([src/shared/models/identity.py:38](src/shared/models/identity.py#L38) 此前仅定义零调用)。**接口面新增 2 path**(openapi.json 重导出随批 3 一并执行——批 2 OCC payload 接缝改了 `/api/status/{task_id}` 实际响应结构需等到 OCC 集成落地再重导出)。**测试基线**:**185 passed, 9 skipped**(批 1 净增 59 测试,含 `compute_fingerprint` 分桶参数化覆盖 bbox / volume / face / undercut / material / is_foam 各边界值 + API 契约 401/403/422/200 路径 + 衰减续期 + 任务归属校验 + ORM 注册收口)。**下一步**:批 2 算法接缝(PartingCandidateGenerator / PartingSchemeScorer / MultiSchemeMoldPlanner 透传 hints + OCC worker payload `experience_hints` 通道)+ 批 3 前端按钮 + 反馈 Dialog + 经验角标渲染。)
|
||||
|
||||
> 2026-09-22(**Pydantic v2 schema 配置升级 + `datetime.utcnow()` 弃用清零**:① 全仓 14 处 `class Config`([src/inventory/schemas](../src/inventory/schemas/))+ [src/shared/services/auth_routes.py](../src/shared/services/auth_routes.py) 三处全部迁移到 `model_config = ConfigDict(from_attributes=True)`;② [src/shared/services/auth_service.py](../src/shared/services/auth_service.py) 中 `datetime.utcnow()` 改用 `datetime.now(timezone.utc)`,消除遗留 `DeprecationWarning`;③ 一次跑通 `pytest tests/ -q` 全量无 deprecation 警告,全仓 `from_attributes=True` 语义保持不变,未触发 OpenAPI 漂移。**测试基线**:**126 passed, 4 skipped**(与上一批次一致,无回归)。)
|
||||
>
|
||||
> 2026-09-22(**D13 锁文件流程固化:镜像引入已清偿 + 落锁流程就绪**:① 新增 [deploy/generate_lockfiles.sh](../deploy/generate_lockfiles.sh) / [.bat](../deploy/generate_lockfiles.bat):在 moldinsight conda 环境(仅项目依赖,**不能**在混装开发栈跑)执行 `pip freeze --exclude pythonocc-core`,产出 `deploy/requirements-{base,moldinsight}.lock.txt`;② [Dockerfile.moldinsight](../deploy/Dockerfile.moldinsight) 注释改为指向生成脚本;③ [docs/OPERATIONS.md](../docs/OPERATIONS.md) §2.1 增加完整流程说明(生成时机 / 命令 / 产物 / 消费方 / 提交策略);④ [tests/test_lockfile_generation.py](../tests/test_lockfile_generation.py) 加锁文件存在性 + 体积契约,默认 skip(仓库单测不阻塞),CI 镜像构建 job 显式 `pytest --run-lockfile-check` 启用 fail-fast。**遗留**:锁文件本身尚未落盘——本机 Miniforge 装的是跨项目开发栈混装环境,污染严重不能直接用 `pip freeze`;须等 CI / 生产机器首次构建 moldinsight 镜像后按流程落锁。**测试基线**:**126 passed, 9 skipped**(默认 4 个原有 skip + D13 新增 5 个 skip;启用 `--run-lockfile-check` 时严格断言 2 项锁文件契约)。)
|
||||
>
|
||||
> 2026-09-02 起(含 09-17 之前基线条目)的完整内容见 [archive/2026-09_status_history.md](archive/2026-09_status_history.md)。
|
||||
>
|
||||
> 2026-09-21(**inventory 仪表盘聚合服务下沉完成:dashboard 薄路由化**:① 新增 [dashboard_service.py](../src/inventory/services/dashboard_service.py),将仪表盘首页所需的基础主数据统计、物料库存总量/总值、待处理采购/销售单数、低库存预警列表等聚合查询从路由层下沉到 service;② [dashboard_routes.py](../src/inventory/api/dashboard_routes.py) 改为单行委托薄路由,inventory 主要业务域路由已基本完成 service orchestration 收口;③ 新增 [test_api_dashboard_service.py](../tests/test_api_dashboard_service.py),覆盖 seeded summary 与低库存预警两条 API 回归。**接口面零变化**(无 openapi 漂移)。**测试基线**:**126 passed, 4 skipped**;新增 dashboard 回归 **2 passed**。)
|
||||
>
|
||||
> 2026-09-21(**inventory 产品域跨模块桥接收口完成:`/api/products/from-task/{task_id}` 下沉至 `product_service`**:① [product_service.py](../src/inventory/services/product_service.py) 新增 `create_product_from_task`,将 ProcessingTask / STPFile 查询、已绑定成品幂等返回、`MI{stp_file_id}` SKU 冲突递增、分析结果摘要拼装、成品创建与 `stp_files.product_id` 回写从路由层下沉到 service;② [product_routes.py](../src/inventory/api/product_routes.py) 现已全量薄路由化,产品域 CRUD / BOM / from-task 三类接口统一改为 service orchestration;③ 扩展 [test_api_product_service.py](../tests/test_api_product_service.py) 与 [tests/conftest.py](../tests/conftest.py),补 `STPFile` / `ProcessingTask` 种子及 from-task 创建、重复调用幂等、任务不存在 404 回归。**接口面零变化**(无 openapi 漂移)。**测试基线**:**124 passed, 4 skipped**;product 域回归现为 **14 passed**。)
|
||||
>
|
||||
> 2026-09-21(**inventory 产品域第二批服务下沉完成:product CRUD / BOM 薄路由化,`from-task` 保持独立**:① 新增 [product_service.py](../src/inventory/services/product_service.py),将产品列表、创建、更新、软删除、BOM 查询与 BOM 替换编排从 [product_routes.py](../src/inventory/api/product_routes.py) 下沉到 service 层;② `product_routes` 中除跨模块的 `/api/products/from-task/{task_id}` 仍保留在路由层外,其余端点已改为薄路由委托,inventory 侧形成 `master_data / material / product / purchase_order / sales_order` 一致的 service orchestration 结构;③ 新增 [test_api_product_service.py](../tests/test_api_product_service.py) 覆盖成品物料成本聚合、创建成品库存上下限归零、重复 SKU 校验、软删除、BOM 明细/重建及多条语义校验路径。**接口面零变化**(无 openapi 漂移)。**测试基线**:**122 passed, 4 skipped**;新增 product 回归 **12 passed**。下一刀再处理 `from-task` 与更深的 moldinsight/BOM 交叉编排。)
|
||||
>
|
||||
> 2026-09-21(**inventory 物料域服务下沉完成:price-history / price-trend / material-supplier 薄路由化**:① 新增 [material_service.py](../src/inventory/services/material_service.py),将物料价格历史、价格趋势、物料-供应商关联与按供应商反查物料的业务编排从路由层下沉到 service 层;② [material_routes.py](../src/inventory/api/material_routes.py) 改为薄路由,仅保留依赖注入、参数校验与 service 调用,inventory 侧继续延续 `inventory_service` / `master_data_service` / `purchase_order_service` / `sales_order_service` 的结构收口方向;③ 新增 [test_api_material_service.py](../tests/test_api_material_service.py) 覆盖价格历史新增、趋势汇总、缺历史 404、供应商关联查询/删除、重复关联与非法物料/供应商校验等回归;④ 顺手修复该链路的两个既有结构问题:`PriceHistoryItem` 未从 [inventory.schemas](../src/inventory/schemas/__init__.py) 导出导致 service 导入失败;异步 ORM 读路径原本依赖 `ph.supplier` / `ms.supplier` / `ms.product` 懒加载,测试环境下触发 `MissingGreenlet`,现统一改为显式 join 构造响应。**接口面零变化**(无 openapi 漂移)。**测试基线**:**110 passed, 4 skipped**;新增物料域回归 **10 passed**。)
|
||||
>
|
||||
> 2026-09-21(**inventory 主数据第一批服务下沉完成:customer / supplier / warehouse 薄路由化**:① 新增 [master_data_service.py](../src/inventory/services/master_data_service.py),将客户/供应商/仓库的列表查询、自动编码(`C`/`S`/`W`)、更新、软删除等 CRUD 编排从路由层下沉到 service 层;② [customer_routes.py](../src/inventory/api/customer_routes.py)、[supplier_routes.py](../src/inventory/api/supplier_routes.py)、[warehouse_routes.py](../src/inventory/api/warehouse_routes.py) 改为薄路由,仅保留依赖注入、参数校验与 service 调用,inventory 侧延续既有 `inventory_service` / `purchase_order_service` / `sales_order_service` 的结构收口方向;③ 新增 [test_api_inventory_master_data.py](../tests/test_api_inventory_master_data.py) 覆盖 customer/supplier/warehouse 的搜索、自动编码、更新回包、软删除与默认仓排序回归;④ 顺手修复 inventory 主数据链路两个既有问题:此前 create/update/delete 只 `flush` 不 `commit`,跨请求 session 下后续读写看不到刚创建实体;时间戳到秒的自动编码在同秒连续创建时会撞唯一约束,现改为微秒粒度编码。**接口面零变化**(无 openapi 漂移)。**测试基线**:**100 passed, 4 skipped**;新增主数据回归 **10 passed**。)
|
||||
|
||||
> 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(**批次 3(API 与代码结构)完成**:① D1 清偿——592 行 advanced_router 拆为 [design_router](../src/moldinsight/api/design_router.py) / [cost_router](../src/moldinsight/api/cost_router.py) / [machining_router](../src/moldinsight/api/machining_router.py) / [export_router](../src/moldinsight/api/export_router.py) 四个子路由(端点路径不变),请求体全量 Pydantic 模型化(`request.json()` 手动解析退役,校验统一 422);② 路由装载失败显式化:`ROUTE_MODULES` 清单 + [route_registry](../src/moldinsight/api/route_registry.py),失败经 `/api/health` 呈现 `degraded` 并列出清单(`pythonocc` 改真实探测),DEBUG 下 fail fast——此前失败仅 WARNING 后静默跳过,进程带病启动不可感知;③ 纯 Python 重计算端点(设计/加工/CAM 打包)统一 `asyncio.to_thread` 投放线程池,不再阻塞事件循环(OCC 仍走单线程 executor,D10 留批次 4);④ `StorageIntegrationService`(867 行)按职责拆为 [task_storage](../src/moldinsight/services/task_storage_service.py) / [analysis_storage](../src/moldinsight/services/analysis_storage_service.py) / [file_history](../src/moldinsight/services/file_history_service.py) 三服务,无调用方死代码 `log_user_activity` 删除;⑤ D14 收尾清偿——`MAX_FILE_SIZE` 接线生效(默认上限 50MB→100MB,以 .env 为准)、celery_app 复用 `Settings.redis_url`(连接串唯一拼装点)。**连带修复**:管理员重置密码改 JSON body `{ new_password }`(原裸 str 参数被解析为 query param,前端两个调用点均发 body,功能端到端断裂)+ [UsersView.vue](../frontend/src/modules/users/UsersView.vue) 同步;Dockerfile.celery 的 FROM 对齐 `gemold-backend:latest`(此前引用不存在的 tag,干净环境 celery 镜像必构建失败)。**接口变更三件套随批完成**:openapi.json 重导出(76 paths)+ 前端 `gen:api` 再生。**连带发现**:`npm run build` 因 vite.config.ts 既有 TS6133 失败(与本项目改动无关,登记 D15)。**测试基线**:**122 passed, 2 skipped**(新增 4 个测试文件共 17 项:[test_advanced_split_contract](../tests/test_advanced_split_contract.py) / [test_route_load_status](../tests/test_route_load_status.py) / [test_config_governance](../tests/test_config_governance.py) / [test_auth_password_reset](../tests/test_auth_password_reset.py);skips 为 alembic / celery 缺失环境)。**下一步**:批次 4(架构演进:共享 ORM 拆分、OCC 吞吐方案,见 [ROADMAP.md](ROADMAP.md) §3.1)。)
|
||||
|
||||
> 2026-09-16(**批次 2(任务一致性模型)完成**:① D7 清偿——Redis 进程内存回退**彻底删除**(写 no-op / 读 None,查询路径自然落 PG),PG 为任务状态单一事实源;批量元数据入库:`processing_tasks` 新增 `batch_id` 列(迁移 `a3f8c2d91e47`,**升级后首次启动自动执行**),`GET /api/batch/{batch_id}` 改为 PG 聚合查询 + `STPFile.user_id` 归属校验,删除 Redis batch key 与内存 dict 双通道;`TaskQueryService` PG 视图与 batch 聚合响应补 `progress` / `current_step`(Redis 不可用时前端仍能看到进度);② D8 清偿——型腔分模失败不再吞异常,任务标 failed 并带明确错误(已提交的几何/网格保留);③ D9 清偿——数据本体写方法只 flush,编排层分阶段原子收口(阶段 A 几何+网格、阶段 B 型腔+HTML+特征+指标+验证、完成时参数随状态一并提交),失败先 rollback 再置 failed;进度/状态更新保留即时 commit(长任务进度可见性);upload/batch/advanced 调用方补显式 commit,STPFile + ProcessingTask 原子落库消除孤儿文件记录。D11 未动(共享卷已兜正确性,留后续批次)。**测试基线**:**105 passed, 1 skipped**(新增 [tests/test_batch_status_pg.py](../tests/test_batch_status_pg.py) 4 项 + [tests/test_redis_no_fallback.py](../tests/test_redis_no_fallback.py) 3 项)。**下一步**:批次 3(API 与代码结构:`_safe_include` 失败显式化、advanced_router 拆分 + Pydantic 请求模型、配置治理,见 [ROADMAP.md](ROADMAP.md) §3.1)。)
|
||||
|
||||
> 2026-09-16(**批次 1(部署正确性)完成**:① D6 清偿——分派入参 `file_path` → `stp_file_id`,处理方按 PG 元数据从 RustFS 下载源文件到任务专属临时目录(RustFS 异常时回退节点本地路径),compose 增 `uploads_data` / `html_data` 共享卷过渡兜底;② D12 清偿——新增 `AUTO_MIGRATE` 开关(默认 true 保持单机行为;多副本设 false 改部署流程单点迁移),迁移脚本与 alembic.ini 补进镜像。**连带发现并修复**:迁移目录 `alembic/` 与 alembic 包重名,应用内 `import alembic` 被遮蔽——启动期自动迁移自引入 alembic 起**从未真正生效**(异常被 init_database 吞掉只打日志),且镜像原本未打包迁移脚本;目录已改名 `migrations/`(alembic.ini + 4 处文档引用同步);③ D13 主体——Dockerfile.moldinsight 改为 conda 运行时原生执行(不再跨镜像拷贝 site-packages),基础镜像 tag 锁定;pip 全量锁文件遗留,随下次镜像构建 `pip freeze` 生成;④ compose 关键项去弱默认:`SECRET_KEY` / `ADMIN_PASSWORD` 改 `${VAR:?}` 强制显式配置(与 OPERATIONS「无默认」声明对齐),`create_admin_user` 对空口令显式报错。**测试基线**:**98 passed, 1 skipped**(新增 [tests/test_deployment_config.py](../tests/test_deployment_config.py);alembic 缺失环境 skip)。**遗留**:D13 pip 锁文件;既有问题待查——Dockerfile.celery `FROM gemold-moldinsight:latest`,而 build.sh 只构建 `gemold-backend` tag,干净机器上 build.sh 的 celery 步骤会失败。**下一步**:批次 2(任务一致性模型,见 [ROADMAP.md](ROADMAP.md) §3.1)。)
|
||||
|
||||
> 2026-09-16(**批次 0(安全与诚实)完成**:① `/api/status/{task_id}` 补 JWT 鉴权 + 任务归属校验(无 token 401 / 他人或无主任务 403 / 不存在 404),归属校验收敛为 `TaskQueryService.ensure_task_access` 供 task_router 与 advanced_router 共用——技术债 [D5 清偿](TECH_DEBT.md);② 上传预检 `pythonocc_available` 从硬编码 true 改为惰性真实探测;③ bcrypt 口令治理:创建侧超 72 字节显式拒绝(此前静默截断改变有效密码),验证侧截断比较(兼容历史哈希 + 避免超长登录 500);④ `SECRET_KEY` 未配置 / `RUSTFS_*` 缺失时惰性校验抛明确错误,代码侧不再有占位弱默认。**顺带修复**:完成态任务未持久化 `analysis_metrics` 时 `/api/status` 组装视图 500(值为 None 时 `.get(key, {})` 默认值不生效)。**测试基线**:pip 无 OCC 环境 **96 passed**(新增 [tests/test_status_endpoint_auth.py](../tests/test_status_endpoint_auth.py) 8 项回归)。status 端点鉴权为接口行为变化,已同步 [API_CONTRACT.md](API_CONTRACT.md) §3.2;openapi.json 重导出仍按既有待办随下次接口变更一并执行。**下一步**:批次 1(D6 RustFS 主链路 + D12 alembic 移出 startup + D13 OCC 镜像,见 [ROADMAP.md](ROADMAP.md) §3.1)。)
|
||||
|
||||
> 2026-09-15(**后端设计审查完成 → 治理计划入文档**:完成 moldinsight 后端设计审查(部署 / 任务一致性 / API / 代码结构),产出治理批次计划入 [ROADMAP.md](ROADMAP.md) §3.1(批次 0–4:安全→部署→一致性→结构→架构);新识别技术债 D5–D14 入 [TECH_DEBT.md](TECH_DEBT.md) §3——含确认安全缺口 `/api/status/{task_id}` 无鉴权、主处理链路依赖节点本地文件路径(API 与 Celery worker 容器无共享卷)等。**下一步**:按批次 0 + 批次 1 的 D6(RustFS 主链路)启动实施。)
|
||||
|
||||
> 最后更新:2026-09-15(**项目规范体系对齐 ipc-chat-cortex**——参考 `ipc-chat-cortex` 的 AGENTS.md + docs 规范重整本文档体系:① [AGENTS.md](../AGENTS.md) 重写——硬约束速览(新增:接口变更三件套 Pydantic→openapi.json→gen:api、配置只走 .env 且关键项不兜底、单数据库刻意设计)+ 代码地图逐文件化 + 开发约定映射表(改什么→同步什么文档);② 新增 [OPERATIONS.md](OPERATIONS.md)(配置来源与优先级 / 本地启动 / Compose / 运维硬性要求)与 [API_CONTRACT.md](API_CONTRACT.md)(端点总览 / 统一约定 / OpenAPI 类型生成流程);③ 本文件改为日志体,原静态内容分流到各归属文档(推荐部署模式→DEPLOYMENT §1,未完成项→ROADMAP/TECH_DEBT)。**验证**:openapi 导出命令实测可用(conda gemold 环境,unified app 76 paths);**待办**:checked-in `openapi.json`(2026-07-27,70 paths)已落后当前代码,下次接口变更时按 [API_CONTRACT.md](API_CONTRACT.md) §4 重导出并 `npm run gen:api`。)
|
||||
|
||||
> 上一条:2026-09-02(**模块化收口 + 文档主骨架建立(基线条目)**:代码侧完成 moldinsight 技术债治理——安全收口(debug/history 权限补齐、任务访问控制收紧)、静默失败修复(`detect-undercuts` 基于真实 shape 重建)、OCC 超时后 executor 重建防毒化全队列、后台任务统一分派、Redis 任务状态改 Hash 原子更新、完成态任务视图缓存、导出缓存与持久化收口、旧入口与死代码删除、Generator 公共接口提取 + 契约测试;详见 [TECH_DEBT.md](TECH_DEBT.md) §2。结构侧完成 `src/entrypoints/` 三入口拆分(moldinsight / inventory / unified)、`shared` 平台能力集中、前端独立 `frontend/` 工程。文档侧建立 `STATUS / ARCHITECTURE / ROADMAP / TECH_DEBT / DEPLOYMENT` 主骨架,README 收敛为唯一导航入口,历史材料迁入 [archive/](archive/README.md)。**测试基线**:本地 pip 环境 **47 passed, 1 skipped**(pythonocc 缺失自动 skip);moldinsight conda + OCC 环境 **88 passed**。inventory 侧少量既有 deprecation warnings 不影响通过。)
|
||||
|
||||
> 此前:2026-09-01(**文档体系专项整理启动**:明确「README 只做导航、每类信息单一归属、历史材料进 archive」的文档治理原则;建立 deployment/ 主题目录与 archive/ 归档目录;部署文档收口为 DEPLOYMENT(入口)+ deployment/LINUX_SETUP(操作)+ deployment/DEPLOY_PORT / PORT_CONFIG(端口补充)三层。)
|
||||
@@ -1,255 +0,0 @@
|
||||
# 存储架构说明
|
||||
|
||||
## 架构概述
|
||||
|
||||
本项目采用 **RustFS (S3兼容)** 作为对象存储和 **PostgreSQL** 作为元数据存储的双层存储架构。
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 应用层 (FastAPI) │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
┌───────▼────────┐ ┌─────────▼─────────┐
|
||||
│ PostgreSQL │ │ RustFS/S3 │
|
||||
│ (元数据) │ │ (对象存储) │
|
||||
│ │ │ │
|
||||
│ - users │ │ - stp-files │
|
||||
│ - stp_files │ │ - geometry │
|
||||
│ - geometry_data│ │ - mold-cavities │
|
||||
│ - mold_cavity │ │ - html-files │
|
||||
│ - features │ │ - user-files │
|
||||
│ - logs │ │ │
|
||||
└────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
## PostgreSQL 数据表
|
||||
|
||||
### 用户管理
|
||||
- `users` - 用户信息(用户名、邮箱、密码等)
|
||||
|
||||
### 文件管理
|
||||
- `stp_files` - STP文件元数据(文件名、哈希、大小、状态等)
|
||||
- `html_files` - HTML报告文件元数据
|
||||
|
||||
### 几何数据
|
||||
- `geometry_data` - 几何分析数据(体积、表面积、边界框等)
|
||||
- `mold_cavity_data` - 模具型腔数据(工艺参数、质量评估等)
|
||||
- `feature_detections` - 特征检测结果(壁厚、拔模角等)
|
||||
- `design_recommendations` - 设计建议(优先级、参数等)
|
||||
|
||||
### 任务和日志
|
||||
- `processing_tasks` - 处理任务记录
|
||||
- `user_activities` - 用户活动日志
|
||||
- `system_logs` - 系统日志
|
||||
|
||||
## RustFS 存储桶
|
||||
|
||||
| 存储桶名称 | 用途 | 存储内容 |
|
||||
|-------------|------|---------|
|
||||
| `moldinsight-stp-files` | STP/STEP文件 | 用户上传的原始3D模型文件 |
|
||||
| `moldinsight-geometry` | 几何数据 | 几何分析结果的JSON数据 |
|
||||
| `moldinsight-mold-cavities` | 模具型腔数据 | 模具设计的详细JSON数据 |
|
||||
| `moldinsight-html` | HTML文件 | 生成的HTML报告文件 |
|
||||
| `moldinsight-user-files` | 用户文件 | 其他用户上传的文件 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 启动 PostgreSQL
|
||||
|
||||
使用 Docker Compose(推荐):
|
||||
```bash
|
||||
docker-compose up -d postgres
|
||||
```
|
||||
|
||||
或手动启动:
|
||||
```bash
|
||||
# 创建数据库
|
||||
createdb moldinsight
|
||||
|
||||
# 运行数据库容器
|
||||
docker run -d \
|
||||
--name postgres \
|
||||
-e POSTGRES_DB=moldinsight \
|
||||
-e POSTGRES_USER=moldinsight_user \
|
||||
-e POSTGRES_PASSWORD=your_password \
|
||||
-p 5432:5432 \
|
||||
postgres:15
|
||||
```
|
||||
|
||||
### 3. 启动 MinIO
|
||||
|
||||
使用 Docker Compose(推荐):
|
||||
```bash
|
||||
docker-compose up -d minio
|
||||
```
|
||||
|
||||
或手动启动:
|
||||
```bash
|
||||
docker run -d \
|
||||
--name minio \
|
||||
-p 9000:9000 \
|
||||
-p 9001:9001 \
|
||||
-e MINIO_ROOT_USER=minioadmin \
|
||||
-e MINIO_ROOT_PASSWORD=minioadmin \
|
||||
minio/minio server /data --console-address ":9001"
|
||||
```
|
||||
|
||||
### 4. 配置环境变量
|
||||
|
||||
```bash
|
||||
# 复制示例配置
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑 .env 文件,修改数据库和MinIO配置
|
||||
nano .env
|
||||
```
|
||||
|
||||
### 5. 初始化数据库和存储
|
||||
|
||||
```bash
|
||||
# 初始化数据库
|
||||
python src/database/init_db.py
|
||||
|
||||
# 初始化MinIO存储
|
||||
python src/storage/init_storage.py
|
||||
```
|
||||
|
||||
### 6. 启动服务
|
||||
|
||||
```bash
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 保存STP文件
|
||||
|
||||
```python
|
||||
from services.storage_integration import storage_integration
|
||||
from database.database import db_manager
|
||||
|
||||
async def upload_file(file_path: str):
|
||||
async with db_manager.get_session() as session:
|
||||
stp_file = await storage_integration.save_stp_file(
|
||||
session=session,
|
||||
file_path=Path(file_path),
|
||||
original_filename="model.stp",
|
||||
user_id=1
|
||||
)
|
||||
print(f"文件已保存,ID: {stp_file.id}")
|
||||
```
|
||||
|
||||
### 保存几何数据
|
||||
|
||||
```python
|
||||
async def save_geometry(stp_file_id: int, geometry_data: dict):
|
||||
async with db_manager.get_session() as session:
|
||||
geo_data = await storage_integration.save_geometry_data(
|
||||
session=session,
|
||||
stp_file_id=stp_file_id,
|
||||
geometry_json=geometry_data,
|
||||
analysis_method="pythonocc"
|
||||
)
|
||||
print(f"几何数据已保存,ID: {geo_data.id}")
|
||||
```
|
||||
|
||||
### 获取文件数据
|
||||
|
||||
```python
|
||||
async def get_file_data(stp_file_id: int):
|
||||
async with db_manager.get_session() as session:
|
||||
data = await storage_integration.get_stp_file_with_data(
|
||||
session=session,
|
||||
stp_file_id=stp_file_id
|
||||
)
|
||||
|
||||
# 访问几何数据
|
||||
geometry = data['geometry_data']
|
||||
print(f"体积: {geometry['volume']}")
|
||||
print(f"表面积: {geometry['surface_area']}")
|
||||
|
||||
# 访问模具型腔数据
|
||||
cavity = data['mold_cavity_data']
|
||||
print(f"模具材料: {cavity['mold_material']}")
|
||||
|
||||
# 访问特征和建议
|
||||
for feature in data['features']:
|
||||
print(f"特征: {feature['feature_type']}")
|
||||
```
|
||||
|
||||
## 数据清理策略
|
||||
|
||||
### MinIO 对象存储
|
||||
- 设置生命周期策略自动删除旧文件
|
||||
- 示例:删除30天前的临时文件
|
||||
|
||||
### PostgreSQL
|
||||
- 定期清理已删除用户的记录
|
||||
- 归档超过6个月的日志数据
|
||||
|
||||
## 监控和维护
|
||||
|
||||
### 检查存储使用情况
|
||||
```bash
|
||||
# MinIO控制台
|
||||
# http://localhost:9001
|
||||
# 用户名: minioadmin
|
||||
# 密码: minioadmin
|
||||
```
|
||||
|
||||
### 数据库备份
|
||||
```bash
|
||||
# 备份数据库
|
||||
pg_dump -h localhost -U moldinsight_user moldinsight > backup.sql
|
||||
|
||||
# 恢复数据库
|
||||
psql -h localhost -U moldinsight_user moldinsight < backup.sql
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### PostgreSQL
|
||||
- 创建适当的索引(已在模型中定义)
|
||||
- 定期运行 VACUUM 和 ANALYZE
|
||||
- 考虑使用连接池(已配置)
|
||||
|
||||
### MinIO
|
||||
- 启用缓存层
|
||||
- 配置CDN分发静态文件
|
||||
- 使用多区域复制
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **更改默认密码**:生产环境必须更改所有默认密码
|
||||
2. **启用TLS**:生产环境启用 HTTPS
|
||||
3. **访问控制**:配置适当的用户权限
|
||||
4. **数据加密**:敏感数据加密存储
|
||||
5. **定期备份**:设置自动备份策略
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 连接MinIO失败
|
||||
```
|
||||
错误: 对象存储连接失败
|
||||
解决: 检查 MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY 配置
|
||||
```
|
||||
|
||||
### 数据库连接失败
|
||||
```
|
||||
错误: 数据库连接失败
|
||||
解决: 检查 DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD 配置
|
||||
```
|
||||
|
||||
### 文件上传失败
|
||||
```
|
||||
错误: STP文件上传失败
|
||||
解决: 检查磁盘空间、网络连接、MinIO权限
|
||||
```
|
||||
@@ -1,827 +0,0 @@
|
||||
# 铝制家电包装泡沫模具分模功能开发任务清单
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **文档名称** | 铝制家电包装泡沫模具分模功能开发任务清单 |
|
||||
| **版本** | 1.0 |
|
||||
| **日期** | 2026-03-13 |
|
||||
| **项目** | geMoldInsight 模具分模功能增强 |
|
||||
|
||||
---
|
||||
|
||||
## 任务总览
|
||||
|
||||
| 阶段 | 任务数 | 预计工期 |
|
||||
|------|--------|----------|
|
||||
| 第一阶段:基础框架 | 8 | 2 周 |
|
||||
| 第二阶段:分模算法 | 10 | 2 周 |
|
||||
| 第三阶段:质量检测 | 6 | 2 周 |
|
||||
| 第四阶段:可视化和交互 | 8 | 2 周 |
|
||||
| 第五阶段:数据接口和测试 | 6 | 1 周 |
|
||||
| **总计** | **38** | **9 周** |
|
||||
|
||||
---
|
||||
|
||||
## 第一阶段:基础框架搭建 (1-2 周)
|
||||
|
||||
### 任务 1.1:创建铝泡沫模具参数类
|
||||
|
||||
**任务描述**:创建专门针对铝泡沫材料的参数配置类
|
||||
|
||||
**详细说明**:
|
||||
- 在 `src/core/mold_generator.py` 中添加 `AluminumFoamMoldParams` 类
|
||||
- 定义铝泡沫专用参数(发泡倍率、目标密度、成型温度等)
|
||||
- 实现参数验证和默认值设置
|
||||
|
||||
**验收标准**:
|
||||
- 参数类包含所有铝泡沫专用参数
|
||||
- 参数验证通过
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 1.2:创建铝泡沫材料数据库
|
||||
|
||||
**任务描述**:建立铝泡沫材料数据库,包含常用材料参数
|
||||
|
||||
**详细说明**:
|
||||
- 创建材料数据库类 `FoamMaterialDatabase`
|
||||
- 添加 AlSi10Mg、AlSi12、纯铝泡沫等常用材料
|
||||
- 支持材料查询和参数获取
|
||||
|
||||
**验收标准**:
|
||||
- 数据库包含至少 5 种铝泡沫材料
|
||||
- 支持按名称查询材料参数
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:1.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 1.3:扩展现有模具生成器
|
||||
|
||||
**任务描述**:扩展 `MoldCavityGenerator` 类支持铝泡沫参数
|
||||
|
||||
**详细说明**:
|
||||
- 添加铝泡沫参数到构造函数
|
||||
- 添加材料设置方法 `set_foam_material()`
|
||||
- 适配现有的分模流程
|
||||
|
||||
**验收标准**:
|
||||
- 可以使用铝泡沫参数创建生成器
|
||||
- 参数正确传递给分模流程
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:1.1, 1.2
|
||||
|
||||
---
|
||||
|
||||
### 任务 1.4:创建参数配置 API 接口
|
||||
|
||||
**任务描述**:添加前端参数配置 API
|
||||
|
||||
**详细说明**:
|
||||
- 在 `src/api/routes.py` 中添加参数相关路由
|
||||
- 实现参数获取、设置、验证接口
|
||||
- 支持参数模板保存和加载
|
||||
|
||||
**验收标准**:
|
||||
- API 可以获取和设置分模参数
|
||||
- 参数验证正确返回错误信息
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:1.3
|
||||
|
||||
---
|
||||
|
||||
### 任务 1.5:前端参数面板开发
|
||||
|
||||
**任务描述**:在 Web 界面中添加分模参数配置面板
|
||||
|
||||
**详细说明**:
|
||||
- 在 `static/vue-app.js` 中添加参数配置组件
|
||||
- 实现滑块、输入框、选择框等控件
|
||||
- 支持参数实时预览
|
||||
|
||||
**验收标准**:
|
||||
- 界面显示所有可配置参数
|
||||
- 参数修改正确提交到后端
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:1.4
|
||||
|
||||
---
|
||||
|
||||
### 任务 1.6:参数模板功能
|
||||
|
||||
**任务描述**:实现参数模板保存和加载功能
|
||||
|
||||
**详细说明**:
|
||||
- 创建预设参数模板(快速、经济、高精度等)
|
||||
- 支持用户保存自定义模板
|
||||
- 模板存储到数据库
|
||||
|
||||
**验收标准**:
|
||||
- 至少 3 个预设模板可用
|
||||
- 用户可以保存和加载模板
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:1.4, 1.5
|
||||
|
||||
---
|
||||
|
||||
### 任务 1.7:参数验证逻辑
|
||||
|
||||
**任务描述**:实现参数合法性验证
|
||||
|
||||
**详细说明**:
|
||||
- 验证数值范围(角度、容差等)
|
||||
- 验证参数组合合法性
|
||||
- 返回详细的验证错误信息
|
||||
|
||||
**验收标准**:
|
||||
- 所有参数都有验证逻辑
|
||||
- 错误信息清晰明了
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:1.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 1.8:阶段一集成测试
|
||||
|
||||
**任务描述**:测试参数系统的完整性
|
||||
|
||||
**详细说明**:
|
||||
- 测试参数设置和获取
|
||||
- 测试参数验证
|
||||
- 测试前端交互
|
||||
|
||||
**验收标准**:
|
||||
- 所有功能正常运行
|
||||
- 无明显 bug
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:1.1-1.7
|
||||
|
||||
---
|
||||
|
||||
## 第二阶段:分模算法优化 (3-4 周)
|
||||
|
||||
### 任务 2.1:改进法向量分析算法
|
||||
|
||||
**任务描述**:改进分型面检测的法向量分析算法
|
||||
|
||||
**详细说明**:
|
||||
- 添加高斯权重计算
|
||||
- 支持多点采样
|
||||
- 识别主分型方向
|
||||
|
||||
**验收标准**:
|
||||
- 算法能正确处理复杂几何产品
|
||||
- 分型方向准确率 > 90%
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.2:实现多分型面检测
|
||||
|
||||
**任务描述**:支持复杂产品的多个分型面
|
||||
|
||||
**详细说明**:
|
||||
- 识别需要多次分模的区域
|
||||
- 正确处理分型面优先级
|
||||
- 生成有序的分型面列表
|
||||
|
||||
**验收标准**:
|
||||
- 能正确检测 2 个以上分型面
|
||||
- 分型面顺序正确
|
||||
|
||||
**预计工时**:12 小时
|
||||
|
||||
**依赖任务**:2.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.3:倒扣区域检测
|
||||
|
||||
**任务描述**:自动识别产品倒扣区域
|
||||
|
||||
**详细说明**:
|
||||
- 分析产品几何特征
|
||||
- 标记倒扣区域位置
|
||||
- 提供处理建议
|
||||
|
||||
**验收标准**:
|
||||
- 能识别常见的倒扣类型
|
||||
- 提供准确的倒扣位置
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:2.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.4:改进拔模角计算
|
||||
|
||||
**任务描述**:实现完整的拔模角计算和应用
|
||||
|
||||
**详细说明**:
|
||||
- 使用 OpenCASCADE 拔模功能
|
||||
- 支持不同拔模方向
|
||||
- 处理拔模干涉
|
||||
|
||||
**验收标准**:
|
||||
- 拔模角正确应用到模型
|
||||
- 无明显变形或错误
|
||||
|
||||
**预计工时**:12 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.5:铝泡沫收缩补偿
|
||||
|
||||
**任务描述**:针对铝泡沫实现特殊的收缩补偿
|
||||
|
||||
**详细说明**:
|
||||
- 基于发泡倍率计算收缩
|
||||
- 多向收缩补偿
|
||||
- 补偿后尺寸验证
|
||||
|
||||
**验收标准**:
|
||||
- 收缩补偿量准确
|
||||
- 补偿后模型无异常
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:1.2
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.6:型腔分离优化
|
||||
|
||||
**任务描述**:改进型腔和型芯的分离算法
|
||||
|
||||
**详细说明**:
|
||||
- 精确的布尔运算
|
||||
- 处理复杂几何
|
||||
- 分离结果验证
|
||||
|
||||
**验收标准**:
|
||||
- 型腔/型芯分离正确
|
||||
- 分离过程无错误
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.7:模具块生成
|
||||
|
||||
**任务描述**:生成完整的模具块结构
|
||||
|
||||
**详细说明**:
|
||||
- 计算模具尺寸
|
||||
- 添加余量
|
||||
- 生成 A/B 板结构
|
||||
|
||||
**验收标准**:
|
||||
- 模具块尺寸合理
|
||||
- 包含必要的结构元素
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:2.6
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.8:分型线平滑处理
|
||||
|
||||
**任务描述**:对分型线进行平滑处理
|
||||
|
||||
**详细说明**:
|
||||
- B 样条曲线拟合
|
||||
- 尖角处理
|
||||
- 平滑度验证
|
||||
|
||||
**验收标准**:
|
||||
- 分型线平滑无毛刺
|
||||
- 保持原始几何精度
|
||||
|
||||
**预计工时**:6 小时
|
||||
|
||||
**依赖任务**:2.2
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.9:算法性能优化
|
||||
|
||||
**任务描述**:优化分模算法性能
|
||||
|
||||
**详细说明**:
|
||||
- 并行计算支持
|
||||
- 缓存优化
|
||||
- 增量计算
|
||||
|
||||
**验收标准**:
|
||||
- 分模时间 < 30 秒
|
||||
- 内存占用 < 1GB
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:2.1-2.8
|
||||
|
||||
---
|
||||
|
||||
### 任务 2.10:阶段二集成测试
|
||||
|
||||
**任务描述**:测试分模算法的完整流程
|
||||
|
||||
**详细说明**:
|
||||
- 使用测试产品验证
|
||||
- 对比不同参数结果
|
||||
- 性能测试
|
||||
|
||||
**验收标准**:
|
||||
- 算法稳定运行
|
||||
- 结果准确合理
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:2.1-2.9
|
||||
|
||||
---
|
||||
|
||||
## 第三阶段:质量检测模块 (5-6 周)
|
||||
|
||||
### 任务 3.1:创建质量检测器类
|
||||
|
||||
**任务描述**:创建 `MoldQualityInspector` 质量检测类
|
||||
|
||||
**详细说明**:
|
||||
- 设计检测器架构
|
||||
- 定义检测接口
|
||||
- 实现结果数据结构
|
||||
|
||||
**验收标准**:
|
||||
- 类结构完整
|
||||
- 接口定义清晰
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 3.2:分模面平滑度检测
|
||||
|
||||
**任务描述**:检测分模面的平滑度
|
||||
|
||||
**详细说明**:
|
||||
- 曲率分析
|
||||
- 凹凸检测
|
||||
- 评分计算
|
||||
|
||||
**验收标准**:
|
||||
- 正确识别不平滑区域
|
||||
- 给出评分 (0-100)
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:3.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 3.3:分模面连续性检测
|
||||
|
||||
**任务描述**:检测分模面的连续性
|
||||
|
||||
**详细说明**:
|
||||
- 边界检查
|
||||
- 间隙检测
|
||||
- 完整性验证
|
||||
|
||||
**验收标准**:
|
||||
- 能识别间隙和断点
|
||||
- 报告位置和大小
|
||||
|
||||
**预计工时**:6 小时
|
||||
|
||||
**依赖任务**:3.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 3.4:模具结构合理性检测
|
||||
|
||||
**任务描述**:检测模具结构的合理性
|
||||
|
||||
**详细说明**:
|
||||
- 模具尺寸检查
|
||||
- 壁厚检查
|
||||
- 干涉检查
|
||||
|
||||
**验收标准**:
|
||||
- 识别所有结构问题
|
||||
- 提供修改建议
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:3.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 3.5:生产可行性评估
|
||||
|
||||
**任务描述**:评估模具的生产可行性
|
||||
|
||||
**详细说明**:
|
||||
- 注塑压力计算
|
||||
- 锁模力计算
|
||||
- 成型周期估算
|
||||
|
||||
**验收标准**:
|
||||
- 估算值在合理范围
|
||||
- 提供改进建议
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:3.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 3.6:质量报告生成
|
||||
|
||||
**任务描述**:生成完整的质量检测报告
|
||||
|
||||
**详细说明**:
|
||||
- 汇总各项检测结果
|
||||
- 生成 PDF 格式报告
|
||||
- 支持导出
|
||||
|
||||
**验收标准**:
|
||||
- 报告内容完整
|
||||
- 格式规范
|
||||
|
||||
**预计工时**:6 小时
|
||||
|
||||
**依赖任务**:3.2-3.5
|
||||
|
||||
---
|
||||
|
||||
## 第四阶段:可视化和交互 (5-6 周)
|
||||
|
||||
### 任务 4.1:分型面可视化增强
|
||||
|
||||
**任务描述**:改进分型面的可视化效果
|
||||
|
||||
**详细说明**:
|
||||
- 分型面颜色和透明度设置
|
||||
- 边缘高亮
|
||||
- 动态效果
|
||||
|
||||
**验收标准**:
|
||||
- 分型面清晰可见
|
||||
- 与产品形成对比
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 4.2:分型线可视化增强
|
||||
|
||||
**任务描述**:改进分型线的可视化
|
||||
|
||||
**详细说明**:
|
||||
- 线条颜色和粗细
|
||||
- 端点标记
|
||||
- 动态绘制效果
|
||||
|
||||
**验收标准**:
|
||||
- 分型线清晰可见
|
||||
- 便于观察细节
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 4.3:交互式分型面调整
|
||||
|
||||
**任务描述**:支持用户拖拽调整分型面
|
||||
|
||||
**详细说明**:
|
||||
- 鼠标拖拽事件
|
||||
- 实时更新模型
|
||||
- 撤销/重做支持
|
||||
|
||||
**验收标准**:
|
||||
- 拖拽响应流畅
|
||||
- 模型正确更新
|
||||
|
||||
**预计工时**:12 小时
|
||||
|
||||
**依赖任务**:4.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 4.4:交互式参数调整
|
||||
|
||||
**任务描述**:支持实时调整参数并预览效果
|
||||
|
||||
**详细说明**:
|
||||
- 滑块实时更新
|
||||
- 参数变化动画
|
||||
- 效果对比
|
||||
|
||||
**验收标准**:
|
||||
- 调整流畅无延迟
|
||||
- 效果正确显示
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:1.5
|
||||
|
||||
---
|
||||
|
||||
### 任务 4.5:剖视图功能
|
||||
|
||||
**任务描述**:添加剖视图功能
|
||||
|
||||
**详细说明**:
|
||||
- 沿分型面剖切
|
||||
- 内部结构显示
|
||||
- 剖面编辑
|
||||
|
||||
**验收标准**:
|
||||
- 剖视图正确显示
|
||||
- 切换流畅
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:4.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 4.6:测量工具
|
||||
|
||||
**任务描述**:添加测量工具
|
||||
|
||||
**详细说明**:
|
||||
- 距离测量
|
||||
- 角度测量
|
||||
- 测量结果标注
|
||||
|
||||
**验收标准**:
|
||||
- 测量结果准确
|
||||
- 操作便捷
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 4.7:视角控制增强
|
||||
|
||||
**任务描述**:改进视角控制
|
||||
|
||||
**详细说明**:
|
||||
- 预设视角
|
||||
- 动画过渡
|
||||
- 自动对准
|
||||
|
||||
**验收标准**:
|
||||
- 视角切换流畅
|
||||
- 自动对准准确
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 4.8:导出视图功能
|
||||
|
||||
**任务描述**:支持导出当前视图
|
||||
|
||||
**详细说明**:
|
||||
- PNG 图片导出
|
||||
- 高清截图
|
||||
- 报告插图
|
||||
|
||||
**验收标准**:
|
||||
- 导出图片清晰
|
||||
- 格式正确
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:4.1-4.7
|
||||
|
||||
---
|
||||
|
||||
## 第五阶段:数据接口和测试 (7-8 周)
|
||||
|
||||
### 任务 5.1:STEP 导出接口
|
||||
|
||||
**任务描述**:实现 STEP 格式导出
|
||||
|
||||
**详细说明**:
|
||||
- 使用 PythonOCC 导出 STEP
|
||||
- 包含分模后模型
|
||||
- 验证导出正确性
|
||||
|
||||
**验收标准**:
|
||||
- 导出文件可被 CAD 打开
|
||||
- 几何正确
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 5.2:JSON 数据导出
|
||||
|
||||
**任务描述**:实现 JSON 格式导出
|
||||
|
||||
**详细说明**:
|
||||
- 导出分模参数
|
||||
- 导出几何数据
|
||||
- 导出质量报告
|
||||
|
||||
**验收标准**:
|
||||
- JSON 格式正确
|
||||
- 数据完整
|
||||
|
||||
**预计工时**:4 小时
|
||||
|
||||
**依赖任务**:无
|
||||
|
||||
---
|
||||
|
||||
### 任务 5.3:PDF 报告导出
|
||||
|
||||
**任务描述**:实现 PDF 格式报告导出
|
||||
|
||||
**详细说明**:
|
||||
- 质量检测报告
|
||||
- 包含图表和说明
|
||||
- 模板支持
|
||||
|
||||
**验收标准**:
|
||||
- PDF 生成成功
|
||||
- 内容完整
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:3.6
|
||||
|
||||
---
|
||||
|
||||
### 任务 5.4:IGES 格式支持
|
||||
|
||||
**任务描述**:添加 IGES 格式支持
|
||||
|
||||
**详细说明**:
|
||||
- IGES 导入
|
||||
- IGES 导出
|
||||
- 格式验证
|
||||
|
||||
**验收标准**:
|
||||
- 导出文件可被 CAM 软件使用
|
||||
|
||||
**预计工时**:6 小时
|
||||
|
||||
**依赖任务**:5.1
|
||||
|
||||
---
|
||||
|
||||
### 任务 5.5:集成测试
|
||||
|
||||
**任务描述**:完整的系统集成测试
|
||||
|
||||
**详细说明**:
|
||||
- 功能测试
|
||||
- 性能测试
|
||||
- 兼容性测试
|
||||
|
||||
**验收标准**:
|
||||
- 所有功能正常运行
|
||||
- 达到性能指标
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:全部
|
||||
|
||||
---
|
||||
|
||||
### 任务 5.6:用户验收测试
|
||||
|
||||
**任务描述**:配合用户进行验收测试
|
||||
|
||||
**详细说明**:
|
||||
- 演示功能
|
||||
- 收集反馈
|
||||
- 修复问题
|
||||
|
||||
**验收标准**:
|
||||
- 用户满意
|
||||
- 达到预期目标
|
||||
|
||||
**预计工时**:8 小时
|
||||
|
||||
**依赖任务**:5.5
|
||||
|
||||
---
|
||||
|
||||
## 任务依赖关系图
|
||||
|
||||
```
|
||||
第一阶段: 基础框架
|
||||
├── 1.1 创建参数类
|
||||
├── 1.2 材料数据库 ──┐
|
||||
├── 1.3 扩展生成器 ──┼── 1.4 API ──┬── 1.5 前端 ──┬── 1.6 模板 ──┬── 1.7 验证 ──→ 1.8 测试
|
||||
│ │ │ │ │
|
||||
└────────────────────┴──────────────┴──────────────┴──────────────┘
|
||||
|
||||
第二阶段: 分模算法
|
||||
│
|
||||
├── 2.1 法向量分析 ──→ 2.2 多分型面 ──→ 2.3 倒扣检测
|
||||
│
|
||||
├── 2.4 拔模角 ──────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
├── 2.5 收缩补偿 ◄──────────────────┐ │
|
||||
│ │ │
|
||||
├── 2.6 型腔分离 ──→ 2.7 模具块 ──→ 2.8 平滑处理 ──→ 2.9 优化 ──→ 2.10 测试
|
||||
│ │
|
||||
└────────────────────────────────────┴────────────────────────────────────┘
|
||||
|
||||
第三阶段: 质量检测
|
||||
│
|
||||
├── 3.1 检测器类 ──→ 3.2 平滑度 ──→ 3.3 连续性 ──→ 3.4 结构 ──→ 3.5 可行性 ──→ 3.6 报告
|
||||
│
|
||||
└────────────────────────────────────┬────────────────────────────────────┘
|
||||
|
||||
第四阶段: 可视化
|
||||
│
|
||||
├── 4.1 分型面 ──→ 4.2 分型线 ──→ 4.3 拖拽 ──→ 4.4 参数调整 ──→ 4.5 剖视
|
||||
│ │ │
|
||||
├── 4.6 测量 ──→ 4.7 视角 ──→ 4.8 导出 ◄─────────────┘
|
||||
│
|
||||
└────────────────────────────────────┬────────────────────────────────────┘
|
||||
|
||||
第五阶段: 接口和测试
|
||||
│
|
||||
├── 5.1 STEP ──→ 5.2 JSON ◄──┐
|
||||
│ │
|
||||
├── 5.3 PDF ◄─────────────────┼── 5.4 IGES ──→ 5.5 集成 ──→ 5.6 验收
|
||||
│ │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 资源分配
|
||||
|
||||
| 角色 | 任务 | 预计工时 |
|
||||
|------|------|----------|
|
||||
| 后端开发 | 1.1-1.4, 2.1-2.10, 3.1-3.6, 5.1-5.4 | 180 小时 |
|
||||
| 前端开发 | 1.5-1.6, 4.1-4.8 | 60 小时 |
|
||||
| 测试 | 1.8, 2.10, 5.5-5.6 | 32 小时 |
|
||||
| **总计** | | **272 小时** |
|
||||
|
||||
---
|
||||
|
||||
## 风险评估
|
||||
|
||||
| 风险 | 影响 | 应对措施 |
|
||||
|------|------|----------|
|
||||
| 算法复杂度高 | 时间延误 | 预留缓冲时间,分阶段交付 |
|
||||
| OpenCASCADE 兼容问题 | 功能受限 | 多种实现方案,准备备选 |
|
||||
| 性能不达标 | 用户体验差 | 持续优化,必要时降级功能 |
|
||||
| 需求变更 | 计划调整 | 敏捷开发,快速迭代 |
|
||||
|
||||
---
|
||||
|
||||
**文档结束**
|
||||
@@ -0,0 +1,298 @@
|
||||
# geMoldInsight 技术债与治理计划(TECH_DEBT)
|
||||
|
||||
> 文档定位:**当前活跃技术债与治理计划的权威文档**。
|
||||
> 本文回答“现在还有哪些重要债务、优先级如何、下一步怎么处理”;不负责维护当前实现状态,当前状态见 [STATUS.md](STATUS.md)。架构边界见 [ARCHITECTURE.md](ARCHITECTURE.md),未来路线见 [ROADMAP.md](ROADMAP.md)。
|
||||
> 本文由归档文档 [archive/MOLDINSIGHT_TECH_DEBT_PLAN.md](archive/MOLDINSIGHT_TECH_DEBT_PLAN.md) 收敛整理而来,保留活跃债务与治理结论,弱化详细实施流水账。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前技术债概览
|
||||
|
||||
当前最主要的技术债集中在三个区域:
|
||||
|
||||
- **moldinsight API 与处理链路的结构收口**
|
||||
- **inventory 复杂业务域的 service 继续下沉**
|
||||
- **文档 / 部署 / 历史语义与当前代码现状未完全一致**
|
||||
|
||||
已经完成的高优先级治理不再作为持续待办反复展开,当前重点聚焦在“还没完成、且值得继续推进”的部分。
|
||||
|
||||
---
|
||||
|
||||
## 2. 已完成的重要治理(主题摘要)
|
||||
|
||||
按主题归类的高价值治理已完成项。每项的具体修复清单 / 迁移号 / 回归测试 / 测试基线见归档:
|
||||
|
||||
- [archive/2026-09_governance_batches.md](archive/2026-09_governance_batches.md):批次 0–4 + 后续专项 + 2026-09-21 inventory 服务下沉 + 2026-09-22 schema/datetime 弃用清零 的完整流水账
|
||||
- [archive/MOLDINSIGHT_TECH_DEBT_PLAN.md](archive/MOLDINSIGHT_TECH_DEBT_PLAN.md):更早的设计审查原始计划
|
||||
|
||||
### 2.1 安全与权限
|
||||
- debug / history 路由补鉴权;任务访问控制收紧;无主数据不再默认放行
|
||||
- `/api/status/{task_id}` 补 JWT 鉴权与归属校验(原 D5 → §3 D5)
|
||||
- bcrypt 超 72 字节显式拒绝 + 截断比较;`SECRET_KEY` / `RUSTFS_*` 缺失明确报错
|
||||
|
||||
### 2.2 静默失败与可用性
|
||||
- `detect-undercuts` 改为基于真实 shape 分析
|
||||
- OCC 超时后重建 executor(短期)→ D10 方案 B 进程化彻底替换
|
||||
- 后台任务统一分派,补强引用与并发控制
|
||||
|
||||
### 2.3 状态存储与缓存
|
||||
- Redis 任务状态改为 Hash 字段级更新,兼容旧格式
|
||||
- 内存回退彻底删除,PG 为任务状态单一事实源(原 D7)
|
||||
- 完成态任务视图缓存;导出缓存与持久化链路收口
|
||||
|
||||
### 2.4 架构与代码清理
|
||||
- 删除旧单体入口与死代码(`db_manager.create_tables` / `log_user_activity` / `CADExporter.export_mold_results` / `getAluminumPrice` 等)
|
||||
- 惰性配置校验,提升可测试性
|
||||
- Generator 公共接口提取 + 契约测试
|
||||
- 共享 ORM 按模块拆分,跨模块桥接收敛为裸 FK 硬规则(ARCHITECTURE §5.1)
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前活跃技术债
|
||||
|
||||
### D1. `advanced_router` 过大,职责混杂 —— 已清偿(2026-09-17,批次 3)
|
||||
|
||||
修复内容:
|
||||
- 592 行的 advanced_router 按职责拆为四个子路由,端点路径全部不变:[design_router.py](../src/moldinsight/api/design_router.py)(布局/冷浇/模架/倒扣)、[cost_router.py](../src/moldinsight/api/cost_router.py)、[machining_router.py](../src/moldinsight/api/machining_router.py)(CAM/碰撞/刀路/电极/仿真)、[export_router.py](../src/moldinsight/api/export_router.py)(导出/下载/建议)
|
||||
- 全部请求体改 Pydantic 模型(`request.json()` 手动解析退役),校验失败统一 422;`_get_cached_import` 上提为 [core_modules.py](../src/moldinsight/api/core_modules.py) 共用
|
||||
- 契约测试:[tests/test_advanced_split_contract.py](../tests/test_advanced_split_contract.py)(路径不丢、鉴权不丢、422 语义、纯计算端点冒烟)
|
||||
- openapi.json 重导出 + 前端 `gen:api`(接口变更三件套随批完成)
|
||||
|
||||
~~原现状 / 影响~~:导出/估算/设计接口混在单文件,边界不清晰、OpenAPI 可读性差、参数校验不统一。
|
||||
|
||||
### D2. 铝价模拟数据未显式标注来源 —— 已清偿(2026-09-18,批次 4 后续专项)
|
||||
|
||||
修复内容(保留编号以维持引用稳定):
|
||||
- [src/moldinsight/services/aluminum_price_service.py](../src/moldinsight/services/aluminum_price_service.py):`get_aluminum_current_price` 响应补 `source: "simulated"`,`get_aluminum_price_history` 逐项补同字段
|
||||
- [frontend/src/modules/home/HomeView.vue](../frontend/src/modules/home/HomeView.vue):按 `source` 字段渲染"模拟数据 · 参考走势,非实时行情"标注(不再硬编码"上海期货交易所"等虚假来源)
|
||||
- 死代码 `getAluminumPrice` 删除(前端此前保留了一份本地硬编码函数,已无调用方)
|
||||
|
||||
~~原现状 / 影响~~:铝价接口返回走势数据但无来源声明,前端原硬编码"上海期货交易所"字样,与实际模拟数据不一致,属虚假来源声明。
|
||||
|
||||
### D3. shared/platform 边界仍需继续收敛 —— 主体已清偿(2026-09-17 批次 4 + 2026-09-18 后续)
|
||||
|
||||
已完成部分(修复文件清单见 [archive/2026-09_governance_batches.md](archive/2026-09_governance_batches.md) 批次 4 / 后续专项):
|
||||
- 共享 ORM(原最强耦合点)按模块拆分:base / identity(shared)+ moldinsight/models + inventory/models;跨模块只允许裸 FK,单模块部署 mapper 可独立配置(详见 [ARCHITECTURE.md](ARCHITECTURE.md) §6.1)
|
||||
- 旧 `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)
|
||||
|
||||
仍保留的收敛方向(低优先级,随实际重构推进):
|
||||
- identity / platform 的边界语义(ROADMAP §2.1):平台表与模块表的命名/注释口径随实际重构推进
|
||||
|
||||
优先级:**P3**(仅剩 identity/platform 语义注释口径)
|
||||
|
||||
### D4. 文档现状 / 规划 / 历史混放 —— 已清偿(2026-09-22)
|
||||
|
||||
修复内容(保留编号以维持引用稳定):
|
||||
- 已建立 `STATUS / ARCHITECTURE / ROADMAP / TECH_DEBT / DEPLOYMENT` 主骨架,每类信息单一归属;历史材料归档至 `docs/archive/`
|
||||
- 本文档 §2 由"按批次回顾"精简为"按主题摘要",修复文件清单 / 迁移号 / 回归测试 / 测试基线等详细流水账整体迁入 [archive/2026-09_governance_batches.md](archive/2026-09_governance_batches.md)(避免与 §3 重复膨胀)
|
||||
- §3 中对历史批次的引用(如 D3 → §2.8)改为 archive 指针;D2 等已清偿项补齐时间戳
|
||||
|
||||
~~原现状 / 影响~~:TECH_DEBT §2 与 §3 内容重复膨胀,文档目录结构清晰度受新成员评估影响。
|
||||
|
||||
### D5. `/api/status/{task_id}` 未鉴权(安全缺口)—— 已清偿(2026-09-16,批次 0)
|
||||
|
||||
修复内容(保留编号以维持 D6–D14 引用稳定):
|
||||
- 端点补 `Depends(get_current_active_user)`;归属校验收敛为 `TaskQueryService.ensure_task_access`,task_router 与 advanced_router 共用(advanced_router 原私有 `_ensure_task_access` 改为委托)
|
||||
- 语义:无 token 401、他人/无主任务 403(无主不等于公共)、任务不存在 404
|
||||
- 回归测试:[tests/test_status_endpoint_auth.py](../tests/test_status_endpoint_auth.py)
|
||||
- 接口行为变化已同步 [API_CONTRACT.md](API_CONTRACT.md) §3.2
|
||||
|
||||
~~原现状 / 影响~~:端点未挂鉴权,匿名可枚举任务号拉取完整分析视图。
|
||||
|
||||
### D6. 主处理链路依赖节点本地文件路径 —— 已清偿(2026-09-16,批次 1)
|
||||
|
||||
修复内容(保留编号以维持引用稳定):
|
||||
- 分派入参收敛为 `stp_file_id`(`dispatch_processing` 与 Celery 任务签名同步变更):处理方按 PG 元数据从 RustFS 下载源文件到任务专属临时目录(保留原始文件名,下游产物命名不变),任务结束即清理([processing_service.py](../src/moldinsight/services/processing_service.py) `_materialize_source_file`)
|
||||
- RustFS 不可用时回退 `STPFile.file_path` 节点本地路径;compose 为 backend / celery 增加共享卷 `uploads_data` / `html_data` 作过渡兜底(HTML 产物跨容器写读同源问题一并兜住,正式修复在 D11)
|
||||
|
||||
~~原现状 / 影响~~:worker 直读 API 节点本地路径,双容器部署必然 `FileNotFoundError`。
|
||||
|
||||
### D7. Redis 降级为进程内 dict,多副本状态不一致 —— 已清偿(2026-09-16,批次 2)
|
||||
|
||||
修复内容(比原建议更彻底:完全删除内存回退,而非仅限 DEBUG):
|
||||
- [redis_task_manager.py](../src/shared/services/redis_task_manager.py) 删除全部 `_fallback_*` 进程内存存储:Redis 不可用时写 no-op、读返回 None(Redis 仅热缓存,任务状态事实源在 PG,缓存缺失不影响正确性)
|
||||
- 批量元数据入库:`processing_tasks` 新增 `batch_id` 列(迁移 `a3f8c2d91e47`),`GET /api/batch/{batch_id}` 改为按列聚合查询 + `STPFile.user_id` 归属校验,删除 Redis batch key 与进程内 dict 双通道
|
||||
- `TaskQueryService` 的 PG 组装视图补 `progress` / `current_step`(Redis 不可用时前端轮询仍能看到进度);batch 聚合响应同步补 `current_step`
|
||||
|
||||
~~原现状 / 影响~~:Redis 故障时状态静默降级各进程内存,多副本互不可见、同任务不同副本读到不同状态。
|
||||
|
||||
### D8. 型腔生成失败被静默标记为 completed —— 已清偿(2026-09-16,批次 2)
|
||||
|
||||
修复内容:
|
||||
- [processing_service.py](../src/moldinsight/services/processing_service.py) `_step_generate_cavity` 不再吞异常:分模失败直接向编排层传播 → 任务 failed(error_message 说明型腔阶段失败);已提交的几何/网格数据保留,用户可凭失败原因重新分析
|
||||
- 未采用 `completed_with_fallback`:多一个状态值会扩散到前端所有状态分支,failed + 明确错误更诚实且成本低
|
||||
|
||||
~~原现状 / 影响~~:型腔失败被吞掉继续主流程,最终 completed,"完成"状态不可信。
|
||||
|
||||
### D9. 持久化事务边界破碎 —— 已清偿(2026-09-16,批次 2)
|
||||
|
||||
修复内容(进度可见性与原子性折中设计):
|
||||
- **数据本体写方法只 flush 不 commit**:`save_stp_file` / `save_geometry_data` / `save_mesh_data` / `save_mold_cavity_data` / `save_html_file` / `save_features_and_recommendations` / `update_task_parameters` / `update_stp_file_analysis_summary` / `_save_analysis_metrics` / `_save_verification_metrics`
|
||||
- **编排层分阶段收口**([processing_service.py](../src/moldinsight/services/processing_service.py)):阶段 A = 几何+网格(解析后确定成果,原子提交);阶段 B = 型腔+HTML+特征+指标+摘要+验证(结果包原子提交);完成时先 flush 任务参数、完成状态提交时一并落库(completed 即完整)
|
||||
- **失败路径先 rollback 再置 failed**:未提交半成品回滚,失败状态单独提交,不出现"completed 但数据残缺"
|
||||
- **保留即时 commit**:`update_task_status` / `update_stp_file_status`(处理中进度需跨事务对外可见,分钟级长任务不能憋在一个大事务里)
|
||||
- 调用方补显式 commit:upload_router / batch_router(分派前置事务,STPFile + ProcessingTask 原子,消除孤儿文件记录)、advanced_router 导出两处
|
||||
|
||||
~~原现状 / 影响~~:各存储方法内部自行 commit,型腔保存失败留半成品数据且任务仍 completed。
|
||||
|
||||
### D10. OCC 全局单线程串行 + 超时重建泄漏线程 —— 已清偿(2026-09-18,方案 B 实施)
|
||||
|
||||
**方案 B(常驻 OCC 进程池,kill-on-timeout 根治泄漏)已实施**(2026-09-18):
|
||||
- `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)(新增;详见 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md) §5)
|
||||
- 超时/崩溃 = terminate() 换新补位——**残留线程泄漏根治**(C++ 栈由进程边界回收);OCC segfault 不再波及 API/worker 主进程
|
||||
- 调用点全部迁移(解析/网格/型腔/分析/倒扣/STEP 转换),TopoDS 形状不跨进程(`generate_cavity` 的方案形状 STEP 导出改在子进程内持久化,返回 export_manifest)
|
||||
- 顺带删除:内存形状缓存链(`_cache_export_shapes` / `get_export_shapes` / `_persist_step_exports`)、`CADExporter.export_mold_results`(零调用方)、shape_loader(→ [stp_materializer.py](../src/moldinsight/services/stp_materializer.py))
|
||||
- 回归测试:[tests/test_occ_process_pool.py](../tests/test_occ_process_pool.py)(OCC-gated,6 例含真实盒体 STP 端到端)
|
||||
|
||||
**方案 A 部署参数落地**(2026-09-18):`CELERY_CONCURRENCY` / `CELERY_MAX_TASKS_PER_CHILD` 进 compose + `.env.example`(`--max-tasks-per-child` 仍保留为进程回收兜底;2026-09-24 起 worker 与后端共用 `gemold-backend` 镜像,Dockerfile.celery 已移除,参数经 compose `command:` 覆盖与环境变量传递)。
|
||||
|
||||
保留为已知约束(非待修缺陷):
|
||||
- 单进程内 OCC 串行是正确性要求(OCC 非线程安全),吞吐扩展走多进程(方案 A/B)
|
||||
- 每个操作从 STP 原件重新加载形状(STEP 重载成本秒级)——进程隔离的设计取舍,见 OCC_THROUGHPUT §1.2/§5
|
||||
|
||||
优先级:~~**P3**~~ **已清偿**
|
||||
|
||||
### D11. HTML 报告本地磁盘与 RustFS 双写双读 —— 已清偿(2026-09-18)
|
||||
|
||||
修复内容:
|
||||
- **写侧**:可视化产物不再落节点本地 `html_output/`——HTMLGenerator 每任务写临时目录([processing_service.py](../src/moldinsight/services/processing_service.py)),`.html` / `_summary.json` / `_data.json` 三件统一裸传 RustFS 报告键 `html/reports/{filename}`(文件名寻址,同源同秒重复分析即覆盖刷新);`HTMLFile` 表仅存元数据
|
||||
- **读侧**:`/html` StaticFiles 挂载删除,新增代理路由 [html_report_router.py](../src/moldinsight/api/html_report_router.py)(挂根路径保持 URL 形状——持久化 cavity JSON 与前端 iframe 均引用 `/html/{filename}`):RustFS 报告键直取 → 遗留 `html/{hash}.json` JSON 包装解析 → 本地卷存量兜底 → 404,防路径穿越(单段文件名校验)
|
||||
- **部署**:celery 服务摘除 `html_data` 卷(不再写本地);Dockerfile.moldinsight 删除 `COPY html_output/`(构建机陈旧报告不再烤进镜像)
|
||||
- **已知约束**(沿用 StaticFiles 时代既定姿态,非新引入):报告路由不做认证——iframe 无法携带 Authorization 头
|
||||
- **2026-09-27 追记**:可视化产物从"每任务一套"改为**每分模方案一套**——`_attach_scheme_previews` 为每个 `candidate_schemes` 生成独立 HTML / `_summary.json` / `_data.json`([processing_service.py](../src/moldinsight/services/processing_service.py)),`scheme["html_file"]` 供前端切方案即切预览;任务级 HTML 复用推荐方案那份(`HTMLFile` 记录仍仅推荐方案一条)。**已知取舍**:网格数据 JSON 每方案复制一份,报告键存储随方案数线性增长(当前 ≤3 方案,接受);存储吃紧时再演进共享产品网格 + 仅差分型腔/模芯,暂不排期
|
||||
- 回归测试:[tests/test_html_report_router.py](../tests/test_html_report_router.py)(8 例:四链路命中、新旧格式记录区分、媒体类型、404、穿越拒绝)
|
||||
|
||||
~~原现状 / 影响~~:可视化 HTML/摘要同时写本地与 RustFS,多副本下 /html 命中结果取决于负载均衡,跨副本文件不共享。
|
||||
|
||||
### D12. 应用启动时自动执行 alembic 迁移
|
||||
|
||||
### D12. 应用启动时自动执行 alembic 迁移 —— 已清偿(2026-09-16,批次 1)
|
||||
|
||||
修复内容:
|
||||
- 新增 `AUTO_MIGRATE` 开关(settings / .env.example / compose 透传):默认 `true` 保持单机开发行为;多副本部署设 `false`,由部署流程单点执行 alembic CLI 或 `python -m shared.database.init_db`
|
||||
- **连带发现并修复两个使自动迁移从未真正生效的缺陷**:
|
||||
1. 迁移目录 `alembic/` 与 alembic 包重名——应用内 `import alembic` 命中本地目录(namespace package)遮蔽真实包,启动期迁移异常被 `init_database` 吞掉只打日志;已改名 `migrations/`(alembic.ini `script_location` 与 4 处文档引用同步)
|
||||
2. 镜像未打包迁移脚本与 alembic.ini,容器内迁移必然失败——Dockerfile.base / Dockerfile.moldinsight 已补 `COPY migrations/` + `COPY alembic.ini`
|
||||
|
||||
### D12. Pydantic v2 弃用项清理 —— 已清偿(2026-09-22)
|
||||
|
||||
修复内容(保留编号以维持引用稳定):
|
||||
- 全仓 14 处 `class Config:` + `from_attributes = True` 统一迁移为 `model_config = ConfigDict(from_attributes=True)`:
|
||||
- [src/inventory/schemas/customer_schemas.py](../src/inventory/schemas/customer_schemas.py) / [supplier_schemas.py](../src/inventory/schemas/supplier_schemas.py) / [warehouse_schemas.py](../src/inventory/schemas/warehouse_schemas.py) / [inventory_schemas.py](../src/inventory/schemas/inventory_schemas.py) / [stock_movement_schemas.py](../src/inventory/schemas/stock_movement_schemas.py) / [product_schemas.py](../src/inventory/schemas/product_schemas.py) / [material_schemas.py](../src/inventory/schemas/material_schemas.py) / [purchase_order_schemas.py](../src/inventory/schemas/purchase_order_schemas.py) / [sales_order_schemas.py](../src/inventory/schemas/sales_order_schemas.py) / [finance_schemas.py](../src/inventory/schemas/finance_schemas.py)
|
||||
- [src/shared/services/auth_routes.py](../src/shared/services/auth_routes.py) UserResponse / RoleResponse / PermissionResponse
|
||||
- 同步收掉 [src/shared/services/auth_service.py](../src/shared/services/auth_service.py) 中 `datetime.utcnow()` 的遗留 deprecation:3 处 token / last_login 写入改用 `datetime.now(timezone.utc)`,与 Pydantic 无关但同属“现代化弃用清理”范畴
|
||||
- 语义保持:仅切换 Pydantic v2 配置语法 + UTC 时区语义,字段 / OpenAPI / JWT 行为零变化
|
||||
- 验证:`pytest tests/ -q` **126 passed, 4 skipped**,deprecation warning 全部清零
|
||||
|
||||
### D13. PythonOCC 镜像引入方式脆弱 + 依赖无版本锁(镜像引入已清偿;锁文件流程已固化,待首次构建落盘)
|
||||
|
||||
现状:
|
||||
- ~~从 conda env 拷贝 site-packages 进 python:3.12-slim~~(2026-09-16 已修正:[Dockerfile.moldinsight](../deploy/Dockerfile.moldinsight) 改为 conda 运行时原生执行,不再跨镜像拷贝;基础镜像 tag 锁定 `condaforge/miniforge3:24.7.1-2`——2026-09-24 由 `continuumio/miniconda3:24.7.1-0` 切换,conda-forge 单渠道无 Anaconda ToS 顾虑、与 CI / 开发机 Miniforge 同源;另锁定 `python:3.12-slim-bookworm`;tag 可用性随下次镜像构建验证)
|
||||
- 锁文件流程已固化(2026-09-22):
|
||||
- 新增 [deploy/generate_lockfiles.sh](../deploy/generate_lockfiles.sh) / [generate_lockfiles.bat](../deploy/generate_lockfiles.bat):在 moldinsight conda 环境(仅项目依赖,**不能**在混装开发栈跑)执行 `pip freeze --exclude pythonocc-core`,产出 `deploy/requirements-{base,moldinsight}.lock.txt`
|
||||
- [Dockerfile.moldinsight](../deploy/Dockerfile.moldinsight) 注释改为指向生成脚本
|
||||
- [docs/OPERATIONS.md](../docs/OPERATIONS.md) §2.1 增加完整流程说明(生成时机 / 命令 / 产物 / 消费方 / 提交策略)
|
||||
- [tests/test_lockfile_generation.py](../tests/test_lockfile_generation.py) 加锁文件存在性 + 体积契约;默认 skip(仓库单测不阻塞),CI 镜像构建 job 显式 `pytest --run-lockfile-check` 启用
|
||||
- **遗留**:锁文件本身尚未落盘——本机 Miniforge 装的是跨项目开发栈混装环境,污染严重不能直接用 `pip freeze`;须等 CI / 生产机器首次构建 moldinsight 镜像后,按流程跑 `bash deploy/generate_lockfiles.sh` 落锁并提交。已存在护栏:CI 镜像构建 job 跑 `--run-lockfile-check` 后若未落盘会 fail-fast,强制流程走通
|
||||
|
||||
优先级:**P2**(流程已固化,剩"首次构建后落盘"一次性产物)
|
||||
|
||||
### D14. 配置漂移:弱默认 / 死配置 / 重复解析 —— 已清偿(2026-09-16 ~ 09-17,批次 1 / 3)
|
||||
|
||||
修复内容:
|
||||
- ~~RUSTFS_* 弱默认~~(批次 0 代码侧去除);~~compose 侧 SECRET_KEY / ADMIN_PASSWORD 弱默认~~(批次 1 改 `${VAR:?}` 强制显式配置)
|
||||
- ~~MAX_FILE_SIZE 死配置~~(批次 3):upload/batch 路由的 `FileHandler` 接 `settings.UPLOAD_DIR / settings.MAX_FILE_SIZE`(此前处理器硬编码 50MB;接线后默认上限变为 100MB,以 .env 为准)
|
||||
- ~~celery_app 重复拼装~~(批次 3):删除自行 load_dotenv + 手拼 REDIS URL,broker/backend 复用 `Settings.redis_url`(新增 property,连接串唯一拼装点)
|
||||
|
||||
优先级:已清偿
|
||||
|
||||
### D15. 前端 `npm run build` 因既有 TS 错误失败(批次 3 连带发现)—— 已清偿(2026-09-17,批次 4)
|
||||
|
||||
修复内容:
|
||||
- [vite.config.ts](../frontend/vite.config.ts) 删除未使用的回调参数 `mode`(TS6133 源头,一行修复);`vue-tsc -b` 实测通过,生产构建链路恢复
|
||||
|
||||
~~原现状 / 影响~~:`vue-tsc -b`(`npm run build` 的类型检查步)因既有 TS6133 失败,前端无法出生产包(与批次 3 改动无关的既有问题)。
|
||||
|
||||
### D17. 算法成熟度距"老师傅经验"差距 + Human-in-Loop 闭环 —— 批 1 已清偿(2026-09-23)
|
||||
|
||||
**背景**:现有算法(分模 / 倒扣 / 评分 / DFM 校验)是 OCC BREP 上的工程启发式,距模具师傅"看完就知道该咋改"的实战经验仍有结构性差距——倒扣邻接聚类缺失、滑块 / 斜顶设计是纯几何启发、DFM 规则库仅 4 条、评分权重拍脑袋(详见 2026-09-22 用户对话评估)。
|
||||
|
||||
**方案**:引入 Human-in-Loop 闭环——老师傅对系统推荐方案给出"采纳 / 调整 / 拒绝"反馈,以"产品指纹 + 工艺参数"为索引跨任务匹配,**下次同指纹产品分析自动消费这些经验**(OCC worker payload 透传 → MultiSchemeMoldPlanner → PartingSchemeScorer 加成)。老师傅的经验以结构化数据沉淀,避免成为"知识库坟墓"。
|
||||
|
||||
**批 1 已完成(数据 + 权限 + 写入 API)**:
|
||||
- 新增 `experience_feedback` 表(alembic head `b7d1f4a92c3e`,32 表迁移),含 fingerprint JSON 列(PG 下 GIN 索引支持 jsonb_path_query)
|
||||
- 3 个权限码(`view_experience_feedback` / `feedback_experience_hint` / `manage_experience_feedback`)+ 新角色 `process_engineer`;admin 角色 permissions 同步补齐
|
||||
- `init_db.py` 幂等 bug 修复——既有 DB 启动期不再跳过新增权限 / 角色补登(`init_permissions` / `init_roles` 改为按 code 比对,新增保留已有 id,避免 FK 引用失效)
|
||||
- 新增端点 `POST /api/tasks/{task_id}/experience-feedback`(提交方案级反馈;`TaskQueryService.ensure_task_access` 归属校验 + `User.has_permission` 全仓首次调用)+ `GET /api/tasks/{task_id}/experience-hints`(按 material_family + is_foam 锚定的历史聚合)
|
||||
- D9 边界遵守:service.flush + 路由 commit;D17 衰减机制:写新反馈时同 `stp_file_id` 整体续期 90 天 TTL(无 celery beat 依赖)
|
||||
- 测试基线:185 passed, 9 skipped(批 1 净增 59 测试)
|
||||
|
||||
**批 2 已完成(算法接缝 + OCC payload 通道)—— 闭环通**:
|
||||
- [parting_candidate_generator.py](src/moldinsight/core/parting_candidate_generator.py) `generate_candidates(..., hints=None)`:`priority_score += weight × 20`,`sample_count ≥ 2 + weight ≥ 0.5` 时 method 标签升级 `human_experience_primary`
|
||||
- [parting_scheme_scorer.py](src/moldinsight/core/parting_scheme_scorer.py) `score_schemes(..., *, hints=None)`:新增 `score_breakdown["human_hint_bonus"]`(`weight × 12`,`sample_count < 2` 时 ×0.5 折半),纳入 total_score;keyword-only 防与位置参数混淆
|
||||
- [multi_scheme_planner.py](src/moldinsight/core/multi_scheme_planner.py) `generate_plan(..., hints=None)`:透传 hints 到下两层,`global_summary.applied_hints` 注入返回供前端展示
|
||||
- [processing_service.py](src/moldinsight/services/processing_service.py) `_step_generate_cavity`:调 `experience_feedback_service.resolve_for_process_params` 拿同指纹 hints,装进 run_occ payload 顶层 `experience_hints`;解析失败回退空 list 不阻塞主流程
|
||||
- [occ_worker.py](src/moldinsight/core/occ_worker.py) `_op_generate_cavity`:`payload.get("experience_hints") or {}` 透传给 `planner.generate_plan`,普通 dict 跨进程 pickle 安全
|
||||
- 测试基线:192 passed, 13 skipped(批 2 净增 7 通过 + 4 OCC-gated skip)
|
||||
|
||||
**批 3 已完成(前端按钮 + Dialog + 经验角标)—— 闭环可视**:
|
||||
- [ResultView.vue:35-47](frontend/src/modules/moldinsight/ResultView.vue#L35-L47) 方案卡片 summary-header 加 `t-tag` 经验角标(`currentAxisHint` computed 按 scheme_axis 索引 hintsByAxis,无 hints 不渲染)
|
||||
- [ResultView.vue:131-138](frontend/src/modules/moldinsight/ResultView.vue#L131-L138) `export-buttons-bar` 加 `👍 老师傅反馈` 按钮(`v-if="canGiveFeedback"` 角色门控:admin 或 process_engineer)
|
||||
- [HumanFeedbackDialog.vue](frontend/src/modules/moldinsight/components/HumanFeedbackDialog.vue) 新组件:t-dialog + t-form + t-radio-group 三选一 + t-textarea;走 `moldinsightApi.submitExperienceFeedback`,成功后 emit `submitted` 让父组件重拉 hints
|
||||
- [shared/api-client.ts:407-444](frontend/src/shared/api-client.ts#L407-L444) `moldinsightApi` 新增 `getExperienceHints` / `submitExperienceFeedback`
|
||||
- 接口变更三件套随批完成:openapi.json 重导出(2 新 path)→ `npm run gen:api` → `npm run build` 通过
|
||||
|
||||
**剩余工作(按需排期)**:
|
||||
- 批 4:衰减机制完善(与 DB 一致性定期核查)+ DFM 规则库独立模块化 + 经验冲突仲裁 UI
|
||||
|
||||
~~原现状 / 影响~~:算法生成的方案与真实工程决策有差距,老师傅每次都要推翻系统建议重来,沉淀经验无结构化路径。
|
||||
|
||||
---
|
||||
|
||||
## 4. 当前推荐治理顺序
|
||||
|
||||
> 注:2026-09-15 后端设计审查后,治理**执行顺序**以 [ROADMAP.md](ROADMAP.md) §3.1 批次计划为准(批次 0–4);D5–D14 的批次归属见该表。本节保留原有优先项作为补充说明。
|
||||
|
||||
### 第一优先级
|
||||
1. ~~`advanced_router` 拆分~~(2026-09-17 批次 3 完成,见 D1)
|
||||
2. ~~高优先级接口补 Pydantic 请求模型~~(2026-09-17 批次 3 完成)
|
||||
3. 文档主骨架收口并减少重复说明
|
||||
|
||||
### 第二优先级
|
||||
4. 铝价模拟数据来源显式化
|
||||
5. 部署历史文档归档
|
||||
6. shared/platform 语义继续收敛(共享 ORM 归属已于批次 4 清偿,剩余为 app_factory 组合职责等,见 D3)
|
||||
7. inventory 服务继续下沉(2026-09-21 已完成第一批主数据 CRUD 收口:customer / supplier / warehouse → `master_data_service`;剩余复杂域如 product / material / dashboard)
|
||||
|
||||
---
|
||||
|
||||
## 5. 治理原则
|
||||
|
||||
### 5.1 先收口接口与边界,再做更大结构调整
|
||||
|
||||
当前最值得继续投入的,不是大规模目录重写,而是:
|
||||
- 先把接口边界、文档边界、部署边界收清楚
|
||||
- 再逐步推进 shared/platform 的后续调整
|
||||
|
||||
### 5.2 优先做“降低长期维护成本”的改动
|
||||
|
||||
优先处理:
|
||||
- 重复逻辑
|
||||
- 模糊边界
|
||||
- 静态契约缺失
|
||||
- 文档漂移风险
|
||||
|
||||
### 5.3 已解决问题不再长期占据主文档中心
|
||||
|
||||
已经完成且稳定的问题,只在本文保留摘要结论;详细实施流水账后续归档,不继续作为主文档主体。
|
||||
|
||||
---
|
||||
|
||||
## 6. 与相关文档的边界
|
||||
|
||||
- 当前项目状态:看 [STATUS.md](STATUS.md)
|
||||
- 当前架构与模块边界:看 [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||
- 后续演进路线:看 [ROADMAP.md](ROADMAP.md)
|
||||
- 部署主题入口:看 [DEPLOYMENT.md](DEPLOYMENT.md)
|
||||
- 原始 moldinsight 细粒度债务记录:看 [archive/MOLDINSIGHT_TECH_DEBT_PLAN.md](archive/MOLDINSIGHT_TECH_DEBT_PLAN.md)
|
||||
@@ -0,0 +1,100 @@
|
||||
# 2026-09 后端设计治理批次实施流水账(归档)
|
||||
|
||||
> 文档定位:**2026-09 设计审查产出的批次 0–4 与后续专项的完整实施流水账**。
|
||||
> 主骨架权威文档见:
|
||||
> - [../STATUS.md](../STATUS.md)(每批次完成的时间点与产物)
|
||||
> - [../TECH_DEBT.md](../TECH_DEBT.md)(活跃债务 / 已清偿项的当前位置)
|
||||
> - [../ROADMAP.md](../ROADMAP.md)(批次计划与执行顺序)
|
||||
> - [../ARCHITECTURE.md](../ARCHITECTURE.md)(平台 / 模块边界当前定稿)
|
||||
|
||||
## 批次 0(2026-09-16,安全与诚实)
|
||||
|
||||
- `/api/status/{task_id}` 补 JWT 鉴权 + 任务归属校验(无 token 401 / 他人或无主任务 403 / 不存在 404)
|
||||
- 归属校验收敛为 `TaskQueryService.ensure_task_access` 供 task_router 与 advanced_router 共用(原 D5)
|
||||
- 上传预检 `pythonocc_available` 从硬编码 true 改为惰性真实探测
|
||||
- bcrypt 口令治理:创建侧超 72 字节显式拒绝、验证侧截断比较
|
||||
- `SECRET_KEY` 未配置 / `RUSTFS_*` 缺失时惰性校验抛明确错误
|
||||
- 完成态任务未持久化 `analysis_metrics` 时 `/api/status` 组装视图 500 修复
|
||||
- 测试基线:96 passed
|
||||
- 回归测试:`tests/test_status_endpoint_auth.py` 8 项
|
||||
- 接口行为变化已同步 [../API_CONTRACT.md](../API_CONTRACT.md) §3.2
|
||||
|
||||
## 批次 1(2026-09-16,部署正确性)
|
||||
|
||||
- 主处理链路改走 RustFS:分派入参 `file_path` → `stp_file_id`(原 D6)
|
||||
- 处理方按 PG 元数据从 RustFS 下载源文件到任务专属临时目录(保留原始文件名,下游产物命名不变),任务结束即清理
|
||||
- RustFS 不可用时回退 `STPFile.file_path` 节点本地路径;compose 为 backend / celery 增加共享卷 `uploads_data` / `html_data` 作过渡兜底
|
||||
- `AUTO_MIGRATE` 开关(settings / .env.example / compose 透传),默认 `true` 保持单机开发行为(原 D12)
|
||||
- 迁移目录 `alembic/` → `migrations/`(修复 `import alembic` 命中本地目录遮蔽真实包的命名冲突)+ Docker 镜像补 `COPY migrations/` + `COPY alembic.ini`
|
||||
- Dockerfile.moldinsight 改为 conda 运行时原生执行,基础镜像 tag 锁定(原 D13 主体)
|
||||
- compose 关键项去弱默认:`SECRET_KEY` / `ADMIN_PASSWORD` 改 `${VAR:?}` 强制显式配置;`create_admin_user` 对空口令显式报错
|
||||
- 测试基线:98 passed, 1 skipped
|
||||
- 回归测试:`tests/test_deployment_config.py`
|
||||
|
||||
## 批次 2(2026-09-16,任务一致性模型)
|
||||
|
||||
- Redis 进程内存回退彻底删除(写 no-op / 读 None,Redis 仅热缓存),PG 为任务状态单一事实源(原 D7)
|
||||
- 批量元数据入库:`processing_tasks` 新增 `batch_id` 列(迁移 `a3f8c2d91e47`),`GET /api/batch/{batch_id}` 改为 PG 聚合查询 + `STPFile.user_id` 归属校验
|
||||
- 删除 Redis batch key 与进程内 dict 双通道
|
||||
- `TaskQueryService` PG 视图与 batch 聚合响应补 `progress` / `current_step`
|
||||
- 型腔分模失败不再吞异常 → 任务 failed(原 D8)
|
||||
- 持久化事务边界收口:数据本体写方法只 flush,编排层分阶段原子收口(阶段 A 几何+网格、阶段 B 型腔+HTML+特征+指标+验证),失败先 rollback 再置 failed(原 D9)
|
||||
- 进度/状态更新保留即时 commit(长任务进度可见性)
|
||||
- 测试基线:105 passed, 1 skipped
|
||||
- 回归测试:`tests/test_batch_status_pg.py` 4 项 + `tests/test_redis_no_fallback.py` 3 项
|
||||
|
||||
## 批次 3(2026-09-17,API 与代码结构)
|
||||
|
||||
- `advanced_router` 按职责拆为 design / cost / machining / export 四个子路由,端点路径不变(原 D1)
|
||||
- 全部请求体改 Pydantic 模型(`request.json()` 手动解析退役),校验失败统一 422
|
||||
- `_get_cached_import` 上提为 `core_modules.py` 共用
|
||||
- 路由装载失败显式化:`ROUTE_MODULES` 清单 + `route_registry`,失败经 `/api/health` 呈现 degraded(`pythonocc` 真实探测),DEBUG 下 fail fast
|
||||
- 纯 Python 重计算端点统一 `asyncio.to_thread` 投放线程池,不再阻塞事件循环
|
||||
- `StorageIntegrationService`(867 行)按职责拆为 TaskStorage / AnalysisStorage / FileHistory 三服务(原 D14 部分)
|
||||
- `MAX_FILE_SIZE` 接线生效、celery_app 复用 `Settings.redis_url`(原 D14 收尾)
|
||||
- 连带修复:管理员重置密码改 JSON body(原裸 str 参数被解析为 query param,前端发 body 必 422);Dockerfile.celery 的 FROM tag 与实际构建 tag 对齐
|
||||
- 接口变更三件套随批完成:openapi.json 重导出(76 paths)+ 前端 `gen:api`
|
||||
- 测试基线:122 passed, 2 skipped
|
||||
- 回归测试:4 个测试文件共 17 项(`test_advanced_split_contract` / `test_route_load_status` / `test_config_governance` / `test_auth_password_reset`)
|
||||
|
||||
## 批次 4(2026-09-17,架构演进)
|
||||
|
||||
- 共享 ORM 按模块拆分(原 D3 主体):891 行 `shared/models/database.py`(31 模型类三类同居)拆为 `shared/models/base.py`(唯一 Base + 归属约定)/ `shared/models/identity.py`(7 表)/ `moldinsight/models/`(9 表)/ `inventory/models/`(catalog/warehouse/trading/finance 15 表)
|
||||
- 三条跨模块 ORM relationship(`User.stp_files` / `STPFile.user` / `STPFile.product`)经全仓核实均无使用方,直接删除;跨模块桥接收敛为裸 FK 硬规则([../ARCHITECTURE.md](../ARCHITECTURE.md) §5.1)
|
||||
- 约 45 处 import 全量改写,无兼容 facade
|
||||
- 全量注册点收敛为 migrations/env.py 与 tests/conftest.py
|
||||
- 零调用方的死方法 `db_manager.create_tables` 一并删除
|
||||
- `_reset_occ_executor` 补 `cancel_futures=True`(旧实现下"慢恢复"的旧线程会继续消化旧队列,与新 executor **并发操作非线程安全的 OCC**,属数据竞争而非单纯泄漏,原 D10 短期治理)
|
||||
- 吞吐路线定稿于 `topics/performance/OCC_THROUGHPUT.md`(短期 A:celery prefork 伸缩 + max-tasks-per-child 兜底;中期 B:run_occ 接口进程化 + kill-on-timeout 根治)
|
||||
- 顺手清偿 D15:`vite.config.ts` 删除未用的 `mode` 参数,`vue-tsc -b` 恢复通过
|
||||
- 回归测试:`tests/test_model_ownership.py`(31 表全量注册 / 单模块独立 mapper 配置 / 旧模块无 facade)
|
||||
- 测试基线:125 passed, 2 skipped
|
||||
- 接口面零变化(无 openapi 重导出)
|
||||
|
||||
## 批次 4 后续专项(2026-09-18)
|
||||
|
||||
- D11 清偿:可视化报告 RustFS 单源化
|
||||
- 写侧 HTMLGenerator 每任务写临时目录,`.html`/`_summary.json`/`_data.json` 三件统一裸传 RustFS 报告键 `html/reports/{filename}`
|
||||
- 读侧 `/html` StaticFiles 本地挂载删除,新增代理路由 `html_report_router.py`(报告键直取 → 遗留 `html/{hash}.json` JSON 包装解析 → 本地卷存量兜底 → 404)
|
||||
- 防路径穿越(单段文件名校验),URL 形状 `/html/{filename}` 不变
|
||||
- 部署:celery 服务摘除 `html_data` 卷;Dockerfile.moldinsight 删除 `COPY html_output/`
|
||||
- 已知约束:报告路由不做认证(iframe 无法携带 Authorization 头)
|
||||
- 删除 `get_stp_file_with_data` 的死数据块
|
||||
- OCC 方案 A 部署参数落地:`CELERY_CONCURRENCY` / `CELERY_MAX_TASKS_PER_CHILD` 进 Dockerfile.celery + compose + .env.example
|
||||
- D2 清偿:铝价响应带 `source: "simulated"`;前端按来源渲染"模拟数据 · 参考走势"标注;死代码 `getAluminumPrice` 删除
|
||||
- D10 方案 B 实施:`run_occ(fn, *args)` → `run_occ(op_name, payload)`;常驻 OCC 进程池 + kill-on-timeout 根治残留线程泄漏
|
||||
- CI 门禁:`.gitea/workflows/ci.yml` 三 job(pytest / 前端构建 / openapi 漂移检测)
|
||||
- 接口变更三件套随批完成:openapi.json 重导出(76→77 paths,新增 `/html/{filename}`)+ 前端 `gen:api` 再生
|
||||
- 测试基线:143 passed, 0 skipped
|
||||
|
||||
## 后续小步治理(2026-09-21 ~ 2026-09-22)
|
||||
|
||||
- 2026-09-21 inventory 业务层继续沉淀批次(4 批):
|
||||
- `customer / supplier / warehouse` → `master_data_service`
|
||||
- `material_routes`(价格历史 / 趋势 / 供应商关联)→ `material_service`
|
||||
- `product_routes`(CRUD / BOM / from-task 跨模块桥接)→ `product_service`
|
||||
- `dashboard_routes`(首页统计 / 低库存预警)→ `dashboard_service`
|
||||
- 2026-09-22 schema / datetime 现代化弃用清零(D12):
|
||||
- 全仓 14 处 `class Config:` + auth_routes 三处全部迁移到 `model_config = ConfigDict(from_attributes=True)`
|
||||
- `auth_service.py` 中 `datetime.utcnow()` 改用 `datetime.now(timezone.utc)`
|
||||
- 测试基线最终落点:126 passed, 4 skipped,零 deprecation warning
|
||||
@@ -0,0 +1,10 @@
|
||||
# 2026-09 之前 STATUS 早段历史(归档)
|
||||
|
||||
> 文档定位:**[../STATUS.md](../STATUS.md) 顶部精简后,2026-09-17 之前条目的完整副本**。
|
||||
> 2026-09-17 及之后的批次(批次 0–4 + 后续专项 + 2026-09-21 inventory 服务下沉 + 2026-09-22 schema/datetime 弃用清零)以摘要形式保留在 [../STATUS.md](../STATUS.md) 顶部,详细流水见 [2026-09_governance_batches.md](2026-09_governance_batches.md)。
|
||||
|
||||
---
|
||||
|
||||
> 2026-09-02(**模块化收口 + 文档主骨架建立(基线条目)**:代码侧完成 moldinsight 技术债治理——安全收口(debug/history 权限补齐、任务访问控制收紧)、静默失败修复(`detect-undercuts` 基于真实 shape 重建)、OCC 超时后 executor 重建防毒化全队列、后台任务统一分派、Redis 任务状态改 Hash 原子更新、完成态任务视图缓存、导出缓存与持久化收口、旧入口与死代码删除、Generator 公共接口提取 + 契约测试;详见 [TECH_DEBT.md](../TECH_DEBT.md) §2。结构侧完成 `src/entrypoints/` 三入口拆分(moldinsight / inventory / unified)、`shared` 平台能力集中、前端独立 `frontend/` 工程。文档侧建立 `STATUS / ARCHITECTURE / ROADMAP / TECH_DEBT / DEPLOYMENT` 主骨架,README 收敛为唯一导航入口,历史材料迁入 [archive/](README.md)。**测试基线**:本地 pip 环境 **47 passed, 1 skipped**(pythonocc 缺失自动 skip);moldinsight conda + OCC 环境 **88 passed**。inventory 侧少量既有 deprecation warnings 不影响通过。)
|
||||
|
||||
> 此前:2026-09-01(**文档体系专项整理启动**:明确「README 只做导航、每类信息单一归属、历史材料进 archive」的文档治理原则;建立 deployment/ 主题目录与 archive/ 归档目录;部署文档收口为 DEPLOYMENT(入口)+ deployment/LINUX_SETUP(操作)+ deployment/DEPLOY_PORT / PORT_CONFIG(端口补充)三层。)
|
||||
@@ -0,0 +1,5 @@
|
||||
# geMoldInsight 后端模块化重构蓝图(归档)
|
||||
|
||||
> 文档定位:**模块化设计蓝图档案 / 补充设计材料**。
|
||||
> 当前架构与边界的默认入口见 [../ARCHITECTURE.md](../ARCHITECTURE.md),当前状态见 [../STATUS.md](../STATUS.md),未来演进路线见 [../ROADMAP.md](../ROADMAP.md)。
|
||||
> 本文保留更完整的模块化设计背景、目标与分阶段思考,用于追溯设计决策,不再保留在 `docs/` 顶层作为默认入口。
|
||||
@@ -0,0 +1,7 @@
|
||||
# 铝制家电包装泡沫模具分模功能开发检查清单(归档)
|
||||
|
||||
> 文档定位:**阶段性检查清单 / 历史材料**,不再作为当前权威文档。
|
||||
> 当前项目状态见 [../STATUS.md](../STATUS.md),当前架构边界见 [../ARCHITECTURE.md](../ARCHITECTURE.md),当前活跃技术债见 [../TECH_DEBT.md](../TECH_DEBT.md)。
|
||||
> 若需了解当前模具分析方向,请优先参考主骨架文档,而不是本文的阶段 checklist。
|
||||
|
||||
本文保留的是 2026-03-13 铝泡沫模具分模功能增强时期的检查清单,用于记录当时的开发跟踪方式。
|
||||
@@ -1,5 +1,8 @@
|
||||
# Confluence 归档目录结构(建议)
|
||||
|
||||
> 文档定位:**外部文档归档结构建议稿**。
|
||||
> 本文描述的是面向 Confluence/知识库归档时的目录建议,不作为当前仓库内文档体系的权威说明。当前仓库文档入口见 [../../README.md](../../README.md) 与 [README.md](README.md)。
|
||||
|
||||
## 业务流程
|
||||
|
||||
- 01 端到端流程(客户订单→采购→到货→生产→交付)
|
||||
@@ -0,0 +1,6 @@
|
||||
# 交付物清单(归档)
|
||||
|
||||
> 文档定位:**一次性交付产物索引 / 历史材料**。
|
||||
> 本文记录某轮分析/整改时的交付物集合,不作为当前项目状态或当前文档导航入口。当前默认入口见 [../../README.md](../../README.md),当前状态见 [../STATUS.md](../STATUS.md)。
|
||||
|
||||
保留本文的目的主要是追溯当时的分析交付范围,而不是指导当前项目维护。
|
||||
@@ -0,0 +1,5 @@
|
||||
# geMoldInsight 演进路线图(原始执行记录,归档)
|
||||
|
||||
> 文档定位:**历史路线与执行记录原文 / 归档材料**。
|
||||
> 当前默认路线文档见 [../ROADMAP.md](../ROADMAP.md),当前状态见 [../STATUS.md](../STATUS.md),当前技术债见 [../TECH_DEBT.md](../TECH_DEBT.md)。
|
||||
> 本文保留较细粒度的历史诊断、执行清单与过程记录,仅用于追溯,不再作为顶层默认文档。
|
||||
@@ -0,0 +1,19 @@
|
||||
# 前端独立部署 + 统一后端入口实施计划(归档)
|
||||
|
||||
> 文档定位:**阶段性实施计划 / 历史材料**,不再作为当前部署权威文档。
|
||||
> 当前部署入口见 [../DEPLOYMENT.md](../DEPLOYMENT.md),Linux 详细部署步骤见 [../deployment/LINUX_SETUP.md](../deployment/LINUX_SETUP.md),当前项目状态见 [../STATUS.md](../STATUS.md)。
|
||||
|
||||
本文保留的是一次针对“前端独立部署 + unified backend”方向的实施计划,用于记录当时的设计思路与迁移目标。
|
||||
|
||||
当前项目的默认阅读方式已经调整为:
|
||||
- 部署主题入口: [../DEPLOYMENT.md](../DEPLOYMENT.md)
|
||||
- 当前推荐方案与当前事实: [../STATUS.md](../STATUS.md)
|
||||
- 架构边界: [../ARCHITECTURE.md](../ARCHITECTURE.md)
|
||||
|
||||
如果你正在查找**当前有效的部署方式**,请不要以本文作为默认依据,而应优先参考上述主文档。
|
||||
|
||||
---
|
||||
|
||||
# 原始内容
|
||||
|
||||
> 目标:在已经切换到“前端独立部署 + 同域反代”的基础上,进一步取消前端 Nginx 对 `/api` 的路径级分流,改为反代到一个真正的 **unified backend**,一次性解决长期维护成本。
|
||||
@@ -0,0 +1,5 @@
|
||||
# moldinsight 模块技术债务分析与重构计划(原始记录,归档)
|
||||
|
||||
> 文档定位:**moldinsight 技术债原始分析与实施记录 / 归档材料**。
|
||||
> 当前默认技术债文档见 [../TECH_DEBT.md](../TECH_DEBT.md),当前状态见 [../STATUS.md](../STATUS.md)。
|
||||
> 本文保留更细粒度的问题清单、实施记录与阶段性说明,仅用于追溯,不再作为顶层默认文档。
|
||||
@@ -1,5 +1,8 @@
|
||||
# 模具制造进销存核心模块分析报告(代码基线:geMoldInsight)
|
||||
|
||||
> 文档定位:**业务分析/审计型报告文档**。
|
||||
> 本文保留一次特定分析基线下的观察结论与流程梳理,不作为当前项目状态或当前架构的权威说明。当前状态见 [../STATUS.md](../STATUS.md),当前架构见 [../ARCHITECTURE.md](../ARCHITECTURE.md),演进路线见 [../ROADMAP.md](../ROADMAP.md)。
|
||||
|
||||
## 0. 范围与术语映射
|
||||
|
||||
- 客户订单(Customer Order):本仓库实现为 SalesOrder(销售订单),其业务语义更贴近“模具订单/按单生产订单”。对应表:`sales_orders`、`sales_order_items`。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 端口配置历史说明(归档)
|
||||
|
||||
> 文档定位:**历史迁移说明 / 不再作为当前部署权威文档**。
|
||||
> 当前部署入口见 [../DEPLOYMENT.md](../DEPLOYMENT.md),Linux 详细部署步骤见 [../deployment/LINUX_SETUP.md](../deployment/LINUX_SETUP.md)。
|
||||
|
||||
本文件保留为历史说明。
|
||||
|
||||
它所描述的“单体应用单一端口配置”思路,已经不再能完整代表当前 geMoldInsight 的模块化架构。
|
||||
|
||||
当前项目已演进为:
|
||||
|
||||
- gemold 模块可独立部署
|
||||
- inventory 模块可独立部署
|
||||
- unified 作为组合模式存在
|
||||
- gemold 与 inventory 应分别考虑端口与网关暴露方式
|
||||
|
||||
因此,端口配置的当前权威说明已转移到以下文档:
|
||||
|
||||
- [../DEPLOYMENT.md](../DEPLOYMENT.md)
|
||||
- [../deployment/LINUX_SETUP.md](../deployment/LINUX_SETUP.md)
|
||||
- [../deployment/DEPLOY_PORT.md](../deployment/DEPLOY_PORT.md)
|
||||
- [../deployment/PORT_CONFIG.md](../deployment/PORT_CONFIG.md)
|
||||
@@ -0,0 +1,37 @@
|
||||
# 文档归档说明(archive)
|
||||
|
||||
> 文档定位:**历史文档与阶段性材料归档目录**。
|
||||
> archive 仅保存历史迁移说明、阶段性计划与已不再作为默认入口的旧文档;当前权威内容请优先查看 `docs/` 主骨架:
|
||||
> - [../STATUS.md](../STATUS.md)
|
||||
> - [../ARCHITECTURE.md](../ARCHITECTURE.md)
|
||||
> - [../ROADMAP.md](../ROADMAP.md)
|
||||
> - [../TECH_DEBT.md](../TECH_DEBT.md)
|
||||
> - [../DEPLOYMENT.md](../DEPLOYMENT.md)
|
||||
|
||||
本目录用于存放:
|
||||
- 历史迁移说明
|
||||
- 阶段性实施计划
|
||||
- 已不再作为默认入口的旧文档
|
||||
|
||||
当前已归档:
|
||||
- [PORT_REFACTOR_SUMMARY.md](PORT_REFACTOR_SUMMARY.md)
|
||||
- [FRONTEND_UNIFIED_DEPLOYMENT_PLAN.md](FRONTEND_UNIFIED_DEPLOYMENT_PLAN.md)
|
||||
- [TASKS_ALUMINUM_FOAM_MOLD.md](TASKS_ALUMINUM_FOAM_MOLD.md)
|
||||
- [CHECKLIST_ALUMINUM_FOAM_MOLD.md](CHECKLIST_ALUMINUM_FOAM_MOLD.md)
|
||||
- [DELIVERABLES.md](DELIVERABLES.md)
|
||||
- [EVOLUTION_ROADMAP.md](EVOLUTION_ROADMAP.md)
|
||||
- [MOLDINSIGHT_TECH_DEBT_PLAN.md](MOLDINSIGHT_TECH_DEBT_PLAN.md)
|
||||
- [BACKEND_MODULARIZATION_BLUEPRINT.md](BACKEND_MODULARIZATION_BLUEPRINT.md)
|
||||
- [MOLD_ERP_ANALYSIS_REPORT.md](MOLD_ERP_ANALYSIS_REPORT.md)
|
||||
- [ZERO_FINISHED_INVENTORY_CERTIFICATE.md](ZERO_FINISHED_INVENTORY_CERTIFICATE.md)
|
||||
- [CONFLUENCE_ARCHIVE_STRUCTURE.md](CONFLUENCE_ARCHIVE_STRUCTURE.md)
|
||||
- [2026-09_governance_batches.md](2026-09_governance_batches.md):2026-09 设计审查批次 0–4 + 后续专项的完整实施流水账([../TECH_DEBT.md](../TECH_DEBT.md) 与 [../STATUS.md](../STATUS.md) 仅保留摘要)
|
||||
- [2026-09_status_history.md](2026-09_status_history.md):[../STATUS.md](../STATUS.md) 顶部精简后,2026-09-17 之前条目的完整副本
|
||||
- [topics/ai/](topics/ai/):已迁移的 AI 相关专题历史材料
|
||||
- [topics/performance/](topics/performance/):已迁移的性能专题历史材料
|
||||
- [topics/aluminum-foam/](topics/aluminum-foam/):已迁移的铝泡沫专题历史材料
|
||||
|
||||
部署相关的归档文档仅用于历史追溯;当前对应入口请查看:
|
||||
- [../DEPLOYMENT.md](../DEPLOYMENT.md)
|
||||
- [../deployment/LINUX_SETUP.md](../deployment/LINUX_SETUP.md)
|
||||
- [../deployment/DEPLOY_PORT.md](../deployment/DEPLOY_PORT.md)
|
||||
@@ -0,0 +1,7 @@
|
||||
# 铝制家电包装泡沫模具分模功能开发任务清单(归档)
|
||||
|
||||
> 文档定位:**阶段性任务清单 / 历史材料**,不再作为当前权威文档。
|
||||
> 当前项目状态见 [../STATUS.md](../STATUS.md),当前架构边界见 [../ARCHITECTURE.md](../ARCHITECTURE.md),当前活跃技术债见 [../TECH_DEBT.md](../TECH_DEBT.md)。
|
||||
> 若需了解当前模具分析方向,请优先参考主骨架文档,而不是本文的阶段任务分解。
|
||||
|
||||
本文保留的是 2026-03-13 铝泡沫模具分模功能增强时期的任务拆解,用于记录当时的实施计划与阶段安排。
|
||||
+3
@@ -1,5 +1,8 @@
|
||||
# “零成品库存”证明报告(geMoldInsight)
|
||||
|
||||
> 文档定位:**特定业务口径下的专题证明/分析报告**。
|
||||
> 本文解释“零成品库存”这一业务与财务口径,不作为当前项目整体状态的权威说明。当前状态见 [../STATUS.md](../STATUS.md),相关业务分析见 [MOLD_ERP_ANALYSIS_REPORT.md](MOLD_ERP_ANALYSIS_REPORT.md)。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
系统不设置成品入库、成品出库、销售退货等成品库存模块;系统库存口径仅覆盖“物料(material)”,成品(finished/模具)仅作为订单交付对象,不进入库存核算链路。
|
||||
@@ -1,5 +1,7 @@
|
||||
# AI 智能引擎设计文档
|
||||
|
||||
> 文档定位:**AI 能力方向的设计性/专题性文档**。
|
||||
> 本文描述的是 AI 引擎的设计设想与能力规划,不作为当前实现状态的权威说明。当前状态见 [../../../STATUS.md](../../../STATUS.md),当前架构边界见 [../../../ARCHITECTURE.md](../../../ARCHITECTURE.md),后续路线见 [../../../ROADMAP.md](../../../ROADMAP.md)。
|
||||
## 一、AI 引擎架构
|
||||
|
||||
### 1.1 整体架构
|
||||
@@ -1,5 +1,7 @@
|
||||
# AI + FreeCAD 集成方案
|
||||
|
||||
> 文档定位:**AI / FreeCAD 集成方向的专题设计文档**。
|
||||
> 本文描述的是集成设想、能力规划与差距分析,不作为当前实现状态的权威说明。当前状态见 [../../../STATUS.md](../../../STATUS.md),当前架构边界见 [../../../ARCHITECTURE.md](../../../ARCHITECTURE.md),后续路线见 [../../../ROADMAP.md](../../../ROADMAP.md)。
|
||||
## 一、项目概述
|
||||
|
||||
本文档描述 geMoldInsight 项目集成 AI 智能引擎和 FreeCAD 的完整方案,实现真实的模具型腔生成和 G 代码输出功能。
|
||||
+3
@@ -1,5 +1,8 @@
|
||||
# 铝制家电包装泡沫模具分模功能技术规格说明书
|
||||
|
||||
> 文档定位:**铝泡沫模具分模方向的专题规格文档**。
|
||||
> 本文保留该方向的需求背景、规格设想与能力边界,不作为当前项目整体状态的权威说明。当前状态见 [../../../STATUS.md](../../../STATUS.md),总体架构见 [../../../ARCHITECTURE.md](../../../ARCHITECTURE.md),活跃技术债见 [../../../TECH_DEBT.md](../../../TECH_DEBT.md)。
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
+2
@@ -1,5 +1,7 @@
|
||||
# 性能基准定义(建议)
|
||||
|
||||
> 文档定位:**性能基准与压测口径的专题参考文档**。
|
||||
> 本文给出建议性性能指标与测试数据口径,不作为当前实现状态的权威说明。当前状态见 [../../../STATUS.md](../../../STATUS.md),后续路线见 [../../../ROADMAP.md](../../../ROADMAP.md)。
|
||||
## 1. 核心接口基准
|
||||
|
||||
| 场景 | 接口 | 指标 |
|
||||
+2
@@ -1,5 +1,7 @@
|
||||
# 性能与扩展性评估补充(模具订单/采购主线)
|
||||
|
||||
> 文档定位:**性能与扩展性方向的专题规划文档**。
|
||||
> 本文描述的是性能评估、慢 SQL 发现、扩展路线等补充规划,不作为当前实现状态的权威说明。当前状态见 [../../../STATUS.md](../../../STATUS.md),后续路线见 [../../../ROADMAP.md](../../../ROADMAP.md)。
|
||||
## 1. 高并发冲突面与加固点
|
||||
|
||||
### 1.1 新增/修改模具订单的锁冲突来源
|
||||
+166
-125
@@ -1,170 +1,211 @@
|
||||
# 部署时端口配置说明
|
||||
# 模块化部署端口说明
|
||||
|
||||
## 生产环境部署端口配置
|
||||
> 文档定位:**当前部署下的端口规划补充说明**。
|
||||
> 部署入口与当前推荐方案见 [../DEPLOYMENT.md](../DEPLOYMENT.md),Linux 部署步骤见 [LINUX_SETUP.md](LINUX_SETUP.md)。
|
||||
> 本文档负责 **当前模块化部署模式** 下的端口暴露、端口规划与 Nginx / 防火墙层面的补充说明,不再以历史单体 `src.main:app` 作为默认前提。
|
||||
|
||||
本项目现在使用统一的端口配置文件 `.env`,部署时需要相应调整配置。
|
||||
当前推荐部署对象:
|
||||
|
||||
### Gunicorn 启动配置
|
||||
- frontend(Nginx,同域入口)
|
||||
- unified backend
|
||||
- moldinsight Celery worker(无 HTTP 端口)
|
||||
|
||||
**推荐方式(使用 .env 配置):**
|
||||
以下基础设施默认由服务器现有服务提供,不在本项目 compose 中重复部署:
|
||||
|
||||
- PostgreSQL
|
||||
- Redis
|
||||
- MinIO / RustFS(moldinsight 需要)
|
||||
|
||||
---
|
||||
|
||||
## 1. 推荐端口规划
|
||||
|
||||
| 组件 | 默认端口 | 说明 |
|
||||
|---|---:|---|
|
||||
| frontend | 80 | 前端 Nginx,同域入口 |
|
||||
| unified backend | 8000 | 当前推荐统一后端 |
|
||||
| moldinsight API | 8000 | 模具分析独立部署时使用 |
|
||||
| inventory API | 8001 | 进销存独立部署时使用 |
|
||||
| PostgreSQL | 5432 | 共享数据库 |
|
||||
| Redis | 6379 | 共享队列/缓存 |
|
||||
| MinIO API | 9000 | 对象存储接口 |
|
||||
| MinIO Console | 9001 | 对象存储控制台 |
|
||||
|
||||
> Celery worker 不直接暴露 HTTP 端口。
|
||||
|
||||
---
|
||||
|
||||
## 2. 三种部署模式下的端口
|
||||
|
||||
### 2.1 moldinsight-only
|
||||
|
||||
- 对外开放:`8000`
|
||||
- 依赖:PostgreSQL、Redis、MinIO/RustFS
|
||||
- 可选:前置 Nginx 暴露 80/443
|
||||
|
||||
### 2.2 inventory-only
|
||||
|
||||
- 对外开放:`8001`
|
||||
- 依赖:PostgreSQL、Redis
|
||||
- 不要求对象存储
|
||||
|
||||
### 2.3 unified
|
||||
|
||||
当前推荐由 unified backend 提供单一后端入口:
|
||||
|
||||
- 对外开放:`8000`(或由前置 Nginx / 网关统一暴露 80/443)
|
||||
- 依赖:PostgreSQL、Redis、MinIO / RustFS
|
||||
- 配套:moldinsight Celery worker 不直接暴露 HTTP 端口
|
||||
|
||||
在生产环境中,仍推荐通过同域 Nginx / 网关统一对外暴露 80/443,再反代到 unified backend。
|
||||
|
||||
---
|
||||
|
||||
## 3. Docker Compose 端口来源
|
||||
|
||||
当前主部署文件:
|
||||
- [docker-compose.yml](../../docker-compose.yml)(unified,默认入口)
|
||||
- [docker-compose.moldinsight.yml](../../docker-compose.moldinsight.yml)
|
||||
- [docker-compose.inventory.yml](../../docker-compose.inventory.yml)
|
||||
|
||||
关键端口映射(默认 10003 / 10004,详见 [.env.example](../../.env.example)):
|
||||
|
||||
- `FRONTEND_PORT` → frontend Nginx 外部端口(浏览器入口,推荐直接访问)
|
||||
- unified backend → **不暴露宿主机端口**,经前端 /api 反代同域访问(docker 网络内 `backend:8000` 互通)
|
||||
- `MOLDINSIGHT_PORT` → moldinsight-only 模式宿主机端口
|
||||
- `INVENTORY_PORT` → inventory-only 模式宿主机端口
|
||||
- `MOLDINSIGHT_PORT` → moldinsight-only 独立部署端口
|
||||
- `INVENTORY_PORT` → inventory-only 独立部署端口
|
||||
|
||||
示例:
|
||||
|
||||
```env
|
||||
MOLDINSIGHT_PORT=10003
|
||||
INVENTORY_PORT=10004
|
||||
```
|
||||
|
||||
对应 compose 行为:
|
||||
|
||||
- moldinsight:`${MOLDINSIGHT_PORT:-10003}:8000`
|
||||
- inventory:`${INVENTORY_PORT:-10004}:8001`
|
||||
|
||||
---
|
||||
|
||||
## 4. 直接运行时的端口约定
|
||||
|
||||
### moldinsight-only
|
||||
|
||||
```bash
|
||||
# 创建启动脚本
|
||||
cat > start_production.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
cd /opt/moldinsight/moldinsight_project
|
||||
source venv/bin/activate
|
||||
|
||||
# 从 .env 读取端口配置
|
||||
if [ -f .env ]; then
|
||||
PORT=$(grep '^PORT=' .env | cut -d'=' -f2)
|
||||
else
|
||||
PORT=8000
|
||||
fi
|
||||
|
||||
# 启动服务
|
||||
gunicorn src.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:${PORT}
|
||||
EOF
|
||||
|
||||
chmod +x start_production.sh
|
||||
./start_production.sh
|
||||
uvicorn src.entrypoints.moldinsight:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### systemd 服务配置
|
||||
### inventory-only
|
||||
|
||||
创建 `/etc/systemd/system/moldinsight.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=MoldInsight Geometry Analysis Service
|
||||
After=network.target postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/opt/moldinsight/moldinsight_project
|
||||
Environment=PATH=/opt/moldinsight/moldinsight_project/venv/bin
|
||||
Environment="PORT=8000"
|
||||
ExecStart=/opt/moldinsight/moldinsight_project/venv/bin/gunicorn src.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:${PORT}
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```bash
|
||||
uvicorn src.entrypoints.inventory:app --host 0.0.0.0 --port 8001
|
||||
```
|
||||
|
||||
**重要:** 在 `[Service]` 部分添加 `Environment="PORT=8000"`,或在 `.env` 文件中配置 `PORT=8000`。
|
||||
如果改端口:
|
||||
- moldinsight 改 `--port`
|
||||
- inventory 改 `--port`
|
||||
- 同步更新 Nginx / 防火墙 / 前端 base URL
|
||||
|
||||
### Nginx 反向代理配置
|
||||
---
|
||||
|
||||
## 5. 前端联动
|
||||
|
||||
如果前端与后端分开部署,需要与前端环境变量保持一致。
|
||||
|
||||
建议前端支持:
|
||||
|
||||
### unified 模式
|
||||
```env
|
||||
VITE_API_BASE_URL=https://api.example.com
|
||||
```
|
||||
|
||||
### split 模式
|
||||
```env
|
||||
VITE_AUTH_API_BASE_URL=https://auth.example.com
|
||||
VITE_MOLDINSIGHT_API_BASE_URL=https://moldinsight.example.com
|
||||
VITE_INVENTORY_API_BASE_URL=https://inventory.example.com
|
||||
```
|
||||
|
||||
当前详细策略见:
|
||||
- [archive/BACKEND_MODULARIZATION_BLUEPRINT.md](../archive/BACKEND_MODULARIZATION_BLUEPRINT.md)
|
||||
|
||||
---
|
||||
|
||||
## 6. Nginx 示例
|
||||
|
||||
### moldinsight-only
|
||||
|
||||
```nginx
|
||||
upstream moldinsight_backend {
|
||||
server 127.0.0.1:8000; # 对应 .env 中的 PORT
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
server_name moldinsight.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://moldinsight_backend;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
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 /static {
|
||||
alias /opt/moldinsight/moldinsight_project/static;
|
||||
}
|
||||
### inventory-only
|
||||
|
||||
location /html_output {
|
||||
alias /opt/moldinsight/moldinsight_project/html_output;
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name inventory.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8001;
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
---
|
||||
|
||||
Docker 部署自动从 `.env` 读取配置,无需额外设置:
|
||||
## 7. 防火墙建议
|
||||
|
||||
如果不通过 Nginx 统一入口而是直接暴露服务端口,则应显式开放:
|
||||
|
||||
```bash
|
||||
# .env 文件配置
|
||||
PORT=8000 # 容器内端口
|
||||
HOST_PORT=8080 # 宿主机端口
|
||||
# moldinsight
|
||||
sudo ufw allow 8000/tcp
|
||||
|
||||
# 启动
|
||||
docker-compose up -d
|
||||
# inventory
|
||||
sudo ufw allow 8001/tcp
|
||||
```
|
||||
|
||||
### 修改生产环境端口
|
||||
生产环境更推荐:
|
||||
- 外部只开放 80/443
|
||||
- 内部仅开放 8000/8001 给 Nginx 或内网访问
|
||||
|
||||
1. **编辑 .env 文件**
|
||||
```bash
|
||||
PORT=9000 # 修改应用端口
|
||||
```
|
||||
---
|
||||
|
||||
2. **重启服务**
|
||||
```bash
|
||||
# systemd
|
||||
sudo systemctl restart moldinsight
|
||||
|
||||
# Docker
|
||||
docker-compose down && docker-compose up -d
|
||||
|
||||
# 手动启动
|
||||
./start_production.sh
|
||||
```
|
||||
|
||||
3. **更新 Nginx 配置(如果使用)**
|
||||
```nginx
|
||||
upstream moldinsight_backend {
|
||||
server 127.0.0.1:9000; # 更新为新端口
|
||||
}
|
||||
```
|
||||
```bash
|
||||
sudo nginx -t && sudo nginx -s reload
|
||||
```
|
||||
|
||||
### 防火墙配置
|
||||
|
||||
如果修改了端口,需要更新防火墙规则:
|
||||
## 8. 快速检查
|
||||
|
||||
```bash
|
||||
# UFW (Ubuntu/Debian)
|
||||
sudo ufw allow 9000/tcp
|
||||
sudo ufw delete allow 8000/tcp # 删除旧端口
|
||||
|
||||
# firewall-cmd (CentOS/RHEL)
|
||||
sudo firewall-cmd --permanent --add-port=9000/tcp
|
||||
sudo firewall-cmd --permanent --remove-port=8000/tcp
|
||||
sudo firewall-cmd --reload
|
||||
curl http://127.0.0.1:8000/health
|
||||
curl http://127.0.0.1:8001/health
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
如果只部署单模块,只检查对应服务即可。
|
||||
|
||||
修改端口后,更新健康检查命令:
|
||||
---
|
||||
|
||||
```bash
|
||||
# 检查服务状态
|
||||
curl http://localhost:9000/health
|
||||
curl http://your-domain.com/health
|
||||
```
|
||||
## 9. 结论
|
||||
|
||||
## 快速参考
|
||||
在当前模块化架构下:
|
||||
|
||||
| 部署方式 | 端口配置位置 | 重启命令 |
|
||||
|---------|------------|---------|
|
||||
| 直接运行 | `.env` 中的 `PORT` | Ctrl+C 后重新运行 |
|
||||
| Gunicorn | `.env` 中的 `PORT` | `systemctl restart moldinsight` |
|
||||
| Docker | `.env` 中的 `PORT` 和 `HOST_PORT` | `docker-compose restart` |
|
||||
| Nginx代理 | Nginx配置中的 `proxy_pass` | `nginx -s reload` |
|
||||
|
||||
## 注意事项
|
||||
|
||||
⚠️ **重要:**
|
||||
1. 所有端口配置统一在 `.env` 文件中管理
|
||||
2. 修改端口后需要同步更新相关配置(Nginx、防火墙等)
|
||||
3. 确保新端口没有被其他服务占用
|
||||
4. 生产环境建议使用 Nginx 反向代理,对外提供 80/443 端口
|
||||
5. .env 文件不应提交到版本控制系统,使用 `.env.example` 作为模板
|
||||
- moldinsight 与 inventory 应视为两个独立后端模块
|
||||
- `unified` 是当前推荐部署模式,由 unified backend 提供单一后端入口
|
||||
- 端口应按模块与部署模式清晰分配;生产环境通常通过同域 Nginx / 网关统一对外暴露 80/443
|
||||
|
||||
+339
-178
@@ -1,177 +1,342 @@
|
||||
# MoldInsight Linux 部署指南
|
||||
# geMoldInsight Linux 部署指南
|
||||
|
||||
## 系统要求
|
||||
- Linux 系统 (Ubuntu 20.04+ / CentOS 8+)
|
||||
- Python 3.8+
|
||||
- PostgreSQL 12+
|
||||
> 文档定位:**Linux 环境下的详细部署操作文档**。
|
||||
> 当前部署主题入口见 [../DEPLOYMENT.md](../DEPLOYMENT.md),当前项目状态见 [../STATUS.md](../STATUS.md),当前架构边界见 [../ARCHITECTURE.md](../ARCHITECTURE.md)。
|
||||
> 本文档描述的是 **当前模块化架构** 下的 Linux 部署方式,而不是历史单体入口方案。
|
||||
|
||||
当前项目支持三种部署模式:
|
||||
|
||||
- **unified**:frontend + unified backend + moldinsight Celery worker,统一对外部署(当前推荐)
|
||||
- **moldinsight-only**:仅部署模具分析后端
|
||||
- **inventory-only**:仅部署进销存后端
|
||||
|
||||
项目保持:
|
||||
|
||||
- **同一 Git 仓库**
|
||||
- **同一 PostgreSQL 数据库**
|
||||
- **共享认证体系**
|
||||
- **按模块独立部署**
|
||||
- **复用服务器上已存在的 PostgreSQL / Redis / RustFS(或 MinIO 兼容存储)**
|
||||
|
||||
详细架构蓝图见:
|
||||
- [archive/BACKEND_MODULARIZATION_BLUEPRINT.md](../archive/BACKEND_MODULARIZATION_BLUEPRINT.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 系统要求
|
||||
|
||||
### 基础要求
|
||||
- Linux(Ubuntu 22.04+ / Debian 12+ / CentOS Stream 9+)
|
||||
- Python 3.12
|
||||
- Git
|
||||
- 服务器上已可访问的 PostgreSQL 15+
|
||||
- 服务器上已可访问的 Redis 7+
|
||||
|
||||
## 1. 环境准备
|
||||
### 按模块附加要求
|
||||
|
||||
#### moldinsight / unified 需要
|
||||
- 服务器上已可访问的 MinIO 或 RustFS 兼容对象存储
|
||||
- PythonOCC 运行环境
|
||||
- Celery worker(推荐与 moldinsight 一起部署)
|
||||
|
||||
#### inventory-only 需要
|
||||
- PostgreSQL
|
||||
- Redis
|
||||
- 不依赖 MinIO / RustFS
|
||||
- 不依赖 Celery worker
|
||||
|
||||
---
|
||||
|
||||
## 2. 代码获取与目录约定
|
||||
|
||||
### 安装系统依赖
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt update
|
||||
sudo apt install python3 python3-pip python3-venv postgresql postgresql-contrib git
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo yum update
|
||||
sudo yum install python3 python3-pip postgresql postgresql-server git
|
||||
```
|
||||
|
||||
### 配置PostgreSQL
|
||||
```bash
|
||||
# 启动PostgreSQL服务
|
||||
sudo systemctl start postgresql
|
||||
sudo systemctl enable postgresql
|
||||
|
||||
# 创建数据库和用户
|
||||
sudo -u postgres psql
|
||||
```
|
||||
|
||||
在PostgreSQL中执行:
|
||||
```sql
|
||||
CREATE DATABASE moldinsight;
|
||||
CREATE USER molduser WITH PASSWORD 'moldpassword';
|
||||
GRANT ALL PRIVILEGES ON DATABASE moldinsight TO molduser;
|
||||
\q
|
||||
```
|
||||
|
||||
## 2. 项目部署
|
||||
|
||||
### 克隆或复制项目
|
||||
```bash
|
||||
# 如果使用Git
|
||||
cd /opt
|
||||
sudo git clone <your-repo-url> moldinsight
|
||||
sudo chown -R $USER:$USER moldinsight
|
||||
cd moldinsight
|
||||
|
||||
# 或者直接复制项目文件到Linux服务器
|
||||
sudo git clone <your-repo-url> geMoldInsight
|
||||
sudo chown -R $USER:$USER geMoldInsight
|
||||
cd geMoldInsight
|
||||
```
|
||||
|
||||
### 创建Python虚拟环境
|
||||
后续示例默认项目位于:
|
||||
|
||||
```bash
|
||||
cd moldinsight_project
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
/opt/geMoldInsight
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
---
|
||||
|
||||
## 3. Python 环境准备
|
||||
|
||||
```bash
|
||||
python3.12 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 3. 环境配置
|
||||
> 如果需要 moldinsight 分析能力,请额外准备 PythonOCC 运行环境。该依赖通常通过 conda 或预构建运行镜像提供,而不是直接由 pip 安装。
|
||||
|
||||
---
|
||||
|
||||
## 4. 环境变量配置
|
||||
|
||||
复制模板:
|
||||
|
||||
### 修改环境配置文件
|
||||
编辑 `.env` 文件:
|
||||
```bash
|
||||
nano .env
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
修改为Linux环境的配置:
|
||||
```env
|
||||
# 数据库配置(Linux环境)
|
||||
DATABASE_URL=postgresql+asyncpg://molduser:moldpassword@localhost:5432/moldinsight
|
||||
或部署场景参考:
|
||||
|
||||
# 服务配置
|
||||
DEBUG=false
|
||||
```bash
|
||||
cp deploy/.env.example .env
|
||||
```
|
||||
|
||||
至少需要配置(并指向服务器上已存在的基础设施):
|
||||
|
||||
```env
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
|
||||
# Redis配置(可选)
|
||||
REDIS_HOST=localhost
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_NAME=moldinsight
|
||||
DB_USER=moldinsight_user
|
||||
DB_PASSWORD=change-me
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# Kafka配置(可选)
|
||||
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
|
||||
KAFKA_SECURITY_PROTOCOL=PLAINTEXT
|
||||
SECRET_KEY=change-me-in-production
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=change-me
|
||||
|
||||
RUSTFS_ENDPOINT=http://127.0.0.1:9000
|
||||
RUSTFS_ACCESS_KEY=minioadmin
|
||||
RUSTFS_SECRET_KEY=minioadmin
|
||||
```
|
||||
|
||||
### 创建必要的目录
|
||||
说明:
|
||||
- `RUSTFS_*` 仅 **moldinsight / unified** 模式需要
|
||||
- `inventory-only` 可不使用对象存储
|
||||
- 当前配置读取实现见 [settings.py](../../src/shared/config/settings.py)
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据库初始化
|
||||
|
||||
项目当前使用 Alembic 管理迁移,应用启动时也会执行初始化流程。
|
||||
|
||||
首次部署建议:
|
||||
|
||||
1. 确保服务器上的 PostgreSQL / Redis / 对象存储服务已可访问
|
||||
2. 启动应用前确认 `.env` 中的 `DB_HOST` / `REDIS_HOST` / `RUSTFS_ENDPOINT` 指向真实服务地址
|
||||
3. 让应用执行初始化/升级逻辑
|
||||
|
||||
相关实现参考:
|
||||
- [init_db.py](../../src/shared/database/init_db.py)
|
||||
|
||||
> 当前项目是 **单数据库** 设计,因此 unified / moldinsight-only / inventory-only 都连接到同一个数据库与同一 migration head。
|
||||
|
||||
---
|
||||
|
||||
## 6. 启动方式
|
||||
|
||||
## 6.0 frontend(同域反代入口)
|
||||
|
||||
当前推荐把前端作为独立静态站点部署,并通过同域 Nginx 反代到 unified backend:
|
||||
|
||||
- `/` → 前端静态资源与 SPA 路由
|
||||
- `/api` → unified backend
|
||||
- `/health` → unified backend
|
||||
- `/html` → unified backend(内部再提供 moldinsight 分析产物)
|
||||
|
||||
如果使用 [docker-compose.yml](../../docker-compose.yml)(unified 模式)的 `frontend` 服务,则该入口已经内置在前端 Nginx 镜像中。
|
||||
|
||||
---
|
||||
|
||||
## 6.1 inventory-only
|
||||
|
||||
```bash
|
||||
mkdir -p uploads html_output logs
|
||||
chmod 755 uploads html_output logs
|
||||
source .venv/bin/activate
|
||||
uvicorn src.entrypoints.inventory:app --host 0.0.0.0 --port 8001
|
||||
```
|
||||
|
||||
## 4. 启动服务
|
||||
适合:
|
||||
- 只提供进销存能力
|
||||
- 不需要模具分析与异步任务
|
||||
|
||||
---
|
||||
|
||||
## 6.2 moldinsight-only
|
||||
|
||||
### 开发模式启动
|
||||
```bash
|
||||
cd moldinsight_project
|
||||
source venv/bin/activate
|
||||
python src/main.py
|
||||
source .venv/bin/activate
|
||||
uvicorn src.entrypoints.moldinsight:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### 生产环境启动(使用Gunicorn)
|
||||
适合:
|
||||
- 单独部署模具分析能力
|
||||
- 文件上传 / 分析 / 导出 / 批量分析
|
||||
|
||||
### moldinsight Celery worker
|
||||
|
||||
建议同时启动 worker:
|
||||
|
||||
```bash
|
||||
# 安装Gunicorn
|
||||
pip install gunicorn uvloop httptools
|
||||
|
||||
# 启动服务
|
||||
cd moldinsight_project
|
||||
source venv/bin/activate
|
||||
gunicorn src.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
|
||||
source .venv/bin/activate
|
||||
celery -A src.celery_app.celery_app worker --loglevel=info
|
||||
```
|
||||
|
||||
## 5. 系统服务配置(可选)
|
||||
> moldinsight 的异步处理链路依赖 Celery + Redis;若只启动 HTTP 服务而不启动 worker,上传分析任务可能无法完整处理。
|
||||
|
||||
---
|
||||
|
||||
## 6.3 unified
|
||||
|
||||
`unified` 是当前推荐的默认部署方式,适合 frontend 同域反代到单一 backend 的本地开发、集成环境与统一部署场景。
|
||||
|
||||
如需按模块独立部署,则使用 `moldinsight-only` 或 `inventory-only` 入口;它们仍共享同一个仓库、同一个数据库与同一套基础设施。
|
||||
|
||||
当前入口与部署编排见:
|
||||
- [../../docker-compose.yml](../../docker-compose.yml)(默认 unified)
|
||||
- [../../docker-compose.moldinsight.yml](../../docker-compose.moldinsight.yml)
|
||||
- [../../docker-compose.inventory.yml](../../docker-compose.inventory.yml)
|
||||
- [../../src/entrypoints/unified.py](../../src/entrypoints/unified.py)
|
||||
|
||||
---
|
||||
|
||||
## 7. systemd 部署示例
|
||||
|
||||
## 7.1 inventory-only 服务
|
||||
|
||||
创建:
|
||||
|
||||
### 创建systemd服务文件
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/moldinsight.service
|
||||
sudo nano /etc/systemd/system/moldinsight-inventory.service
|
||||
```
|
||||
|
||||
添加以下内容:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=MoldInsight Geometry Analysis Service
|
||||
After=network.target postgresql.service
|
||||
Description=geMoldInsight Inventory API
|
||||
After=network.target postgresql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/opt/moldinsight/moldinsight_project
|
||||
Environment=PATH=/opt/moldinsight/moldinsight_project/venv/bin
|
||||
ExecStart=/opt/moldinsight/moldinsight_project/venv/bin/gunicorn src.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
|
||||
WorkingDirectory=/opt/geMoldInsight
|
||||
Environment=PATH=/opt/geMoldInsight/.venv/bin
|
||||
ExecStart=/opt/geMoldInsight/.venv/bin/uvicorn src.entrypoints.inventory:app --host 0.0.0.0 --port 8001
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### 启用并启动服务
|
||||
启用:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable moldinsight
|
||||
sudo systemctl start moldinsight
|
||||
sudo systemctl status moldinsight
|
||||
sudo systemctl enable moldinsight-inventory
|
||||
sudo systemctl start moldinsight-inventory
|
||||
```
|
||||
|
||||
## 6. Nginx反向代理配置(可选)
|
||||
---
|
||||
|
||||
## 7.2 moldinsight-only API 服务
|
||||
|
||||
创建:
|
||||
|
||||
### 安装Nginx
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install nginx
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo yum install nginx
|
||||
sudo nano /etc/systemd/system/moldinsight-moldinsight.service
|
||||
```
|
||||
|
||||
### 创建Nginx配置文件
|
||||
```ini
|
||||
[Unit]
|
||||
Description=geMoldInsight MoldInsight API
|
||||
After=network.target postgresql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/opt/geMoldInsight
|
||||
Environment=PATH=/opt/geMoldInsight/.venv/bin
|
||||
ExecStart=/opt/geMoldInsight/.venv/bin/uvicorn src.entrypoints.moldinsight:app --host 0.0.0.0 --port 8000
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7.3 moldinsight Celery worker 服务
|
||||
|
||||
创建:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/moldinsight
|
||||
sudo nano /etc/systemd/system/moldinsight-celery.service
|
||||
```
|
||||
|
||||
添加以下内容:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=geMoldInsight MoldInsight Celery Worker
|
||||
After=network.target postgresql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/opt/geMoldInsight
|
||||
Environment=PATH=/opt/geMoldInsight/.venv/bin
|
||||
ExecStart=/opt/geMoldInsight/.venv/bin/celery -A src.celery_app.celery_app worker --loglevel=info
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Nginx / 前端同域反代示例
|
||||
|
||||
当前仓库已提供前端 Nginx 配置:
|
||||
- [deploy/nginx/frontend.conf](../../deploy/nginx/frontend.conf)
|
||||
|
||||
如果不使用仓库内 `frontend` 容器,也应遵循同样原则:
|
||||
- `/` 提供前端静态资源与 SPA fallback
|
||||
- `/api/` 反代后端
|
||||
- `/health` 反代后端
|
||||
- `/html/` 反代 moldinsight
|
||||
|
||||
### 8.1 inventory-only
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
server_name inventory.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8001;
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 moldinsight-only
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name moldinsight.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
@@ -180,102 +345,98 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /static {
|
||||
alias /opt/moldinsight/moldinsight_project/static;
|
||||
expires 30d;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 启用站点并重启Nginx
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/moldinsight /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl restart nginx
|
||||
```
|
||||
### 8.3 unified(示意)
|
||||
|
||||
## 7. 防火墙配置
|
||||
统一对外时,可由网关做模块路由,也可直接暴露统一组合后的应用入口。组合层标准化完成前,更推荐通过部署编排或网关来实现统一对外访问。
|
||||
|
||||
---
|
||||
|
||||
## 9. 验证部署
|
||||
|
||||
### 9.1 健康检查
|
||||
|
||||
inventory-only:
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian (ufw)
|
||||
sudo ufw allow 80
|
||||
sudo ufw allow 8000
|
||||
sudo ufw allow ssh
|
||||
sudo ufw enable
|
||||
|
||||
# CentOS/RHEL (firewalld)
|
||||
sudo firewall-cmd --permanent --add-port=80/tcp
|
||||
sudo firewall-cmd --permanent --add-port=8000/tcp
|
||||
sudo firewall-cmd --permanent --add-service=ssh
|
||||
sudo firewall-cmd --reload
|
||||
curl http://127.0.0.1:8001/health
|
||||
```
|
||||
|
||||
## 8. 验证部署
|
||||
moldinsight-only:
|
||||
|
||||
### 检查服务状态
|
||||
```bash
|
||||
# 检查应用服务
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# 检查数据库连接
|
||||
sudo -u postgres psql -d moldinsight -c "SELECT version();"
|
||||
curl http://127.0.0.1:8000/health
|
||||
```
|
||||
|
||||
### 测试文件上传
|
||||
访问 `http://your-server-ip:8000` 上传STP文件测试功能。
|
||||
### 9.2 基础能力验证
|
||||
|
||||
## 9. 故障排除
|
||||
#### inventory-only
|
||||
- 登录接口可用
|
||||
- `/api/products` 返回数据
|
||||
- `/api/inventory` 返回数据
|
||||
|
||||
### 常见问题
|
||||
#### moldinsight-only
|
||||
- 登录接口可用
|
||||
- `/api/upload` 可访问
|
||||
- 上传后 worker 能正常消费任务
|
||||
- `/api/status/{task_id}` 能看到进度
|
||||
|
||||
1. **数据库连接失败**
|
||||
- 检查PostgreSQL服务状态:`sudo systemctl status postgresql`
|
||||
- 验证数据库连接:`psql -h localhost -U molduser -d moldinsight`
|
||||
---
|
||||
|
||||
2. **端口被占用**
|
||||
- 检查端口使用:`netstat -tulpn | grep 8000`
|
||||
- 修改端口或停止占用进程
|
||||
## 10. 常见问题
|
||||
|
||||
3. **权限问题**
|
||||
- 确保目录权限正确:`chmod 755 uploads html_output logs`
|
||||
- 检查文件所有者:`ls -la`
|
||||
### 1. 为什么不再推荐 `src.main:app`?
|
||||
因为当前项目已演进为模块化结构,`src.main:app` 更适合作为过渡兼容入口,而不是长期部署标准。应优先围绕 [entrypoints/](../../src/entrypoints/) 部署。
|
||||
|
||||
4. **依赖安装失败**
|
||||
- 更新pip:`pip install --upgrade pip`
|
||||
- 使用国内镜像:`pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple`
|
||||
### 2. inventory-only 为什么不需要对象存储?
|
||||
因为对象存储主要服务于 moldinsight 分析产物(HTML、导出文件等)。纯 inventory 部署不需要这部分基础设施。
|
||||
|
||||
## 10. 备份和恢复
|
||||
### 3. moldinsight-only 为什么建议同时部署 Celery?
|
||||
因为模具分析任务通常走异步处理链路,仅启动 API 而不启动 worker,会影响上传后的任务处理。
|
||||
|
||||
---
|
||||
|
||||
## 11. Docker Compose 说明
|
||||
|
||||
仓库根目录维护 3 个独立 compose 文件,按文件名映射部署模式:
|
||||
|
||||
| 模式 | Compose 文件 | 一键命令 |
|
||||
|---|---|---|
|
||||
| unified(默认) | [docker-compose.yml](../../docker-compose.yml) | `docker compose up -d` |
|
||||
| moldinsight-only | [docker-compose.moldinsight.yml](../../docker-compose.moldinsight.yml) | `docker compose -f docker-compose.moldinsight.yml up -d` |
|
||||
| inventory-only | [docker-compose.inventory.yml](../../docker-compose.inventory.yml) | `docker compose -f docker-compose.inventory.yml up -d` |
|
||||
|
||||
> 注意:各 service 均未声明 `profiles`,旧 `--profile full/moldinsight/inventory` 写法不再是模式开关(声明了 profiles 的服务在裸 `up` 下不会被选中,会报 `no service selected`);模式切换统一用上表 `-f` 命令。
|
||||
|
||||
不同模式分别包含的服务:
|
||||
|
||||
- **unified**:`frontend` + `backend`(unified 入口)+ `moldinsight-celery`
|
||||
- **moldinsight-only**:`moldinsight`(独立 API)+ `moldinsight-celery`
|
||||
- **inventory-only**:仅 `inventory`
|
||||
|
||||
任一 compose 文件**都不会**再拉起:
|
||||
|
||||
- PostgreSQL
|
||||
- Redis
|
||||
- MinIO / RustFS
|
||||
|
||||
这些基础设施应由服务器现有服务提供,并通过 `.env` 传入连接信息;前端则由 `frontend` 容器独立提供(仅 unified 模式包含),通过同域反代转发到后端。
|
||||
|
||||
镜像构建:
|
||||
|
||||
### 数据库备份
|
||||
```bash
|
||||
# 备份数据库
|
||||
sudo -u postgres pg_dump moldinsight > moldinsight_backup.sql
|
||||
|
||||
# 恢复数据库
|
||||
sudo -u postgres psql -d moldinsight < moldinsight_backup.sql
|
||||
bash deploy/build.sh # base → backend → frontend(celery 复用 backend 镜像)
|
||||
# 或让 compose 构建:docker compose up -d --build
|
||||
# 注意:docker compose up -d 对本地已有同名镜像不会自动重建,更新代码后需 --build
|
||||
```
|
||||
|
||||
### 文件备份
|
||||
```bash
|
||||
# 备份上传的文件和配置
|
||||
tar -czf moldinsight_backup.tar.gz uploads/ html_output/ .env requirements.txt
|
||||
```
|
||||
---
|
||||
|
||||
## 快速启动脚本
|
||||
## 12. 推荐阅读
|
||||
|
||||
创建启动脚本 `start.sh`:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
cd /opt/moldinsight/moldinsight_project
|
||||
source venv/bin/activate
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
赋予执行权限:
|
||||
```bash
|
||||
chmod +x start.sh
|
||||
./start.sh
|
||||
```
|
||||
|
||||
现在您的MoldInsight项目已经可以在Linux环境下正常运行!
|
||||
- [README.md](../../README.md)
|
||||
- [archive/BACKEND_MODULARIZATION_BLUEPRINT.md](../archive/BACKEND_MODULARIZATION_BLUEPRINT.md)
|
||||
- [DEPLOY_PORT.md](./DEPLOY_PORT.md)
|
||||
- [PORT_CONFIG.md](./PORT_CONFIG.md)
|
||||
|
||||
+134
-78
@@ -1,107 +1,163 @@
|
||||
# 端口配置说明
|
||||
# 端口配置说明(模块化架构)
|
||||
|
||||
## ⚠️ 重要提示
|
||||
> 文档定位:**模块化部署下的端口与环境变量配置补充说明**。
|
||||
> 当前部署主题入口见 [../DEPLOYMENT.md](../DEPLOYMENT.md),详细 Linux 部署步骤见 [LINUX_SETUP.md](LINUX_SETUP.md)。
|
||||
> 本文只补充环境变量、端口配置项与 direct run / compose 的映射;端口规划与对外暴露方式以 [DEPLOY_PORT.md](./DEPLOY_PORT.md) 为准。
|
||||
|
||||
本项目现在采用**统一端口配置管理**,所有端口配置集中在一个地方:
|
||||
当前配置中主要需要区分:
|
||||
|
||||
**唯一修改端口的地方:** `.env` 文件中的端口配置部分
|
||||
- **moldinsight API 端口**
|
||||
- **inventory API 端口**
|
||||
- **数据库 / Redis / 对象存储端口**
|
||||
- **前端访问地址**
|
||||
|
||||
## 配置说明
|
||||
---
|
||||
|
||||
编辑项目根目录下的 `.env` 文件:
|
||||
## 1. 当前主配置位置
|
||||
|
||||
```bash
|
||||
# ================================
|
||||
# 端口配置 - 唯一修改端口的地方
|
||||
# ================================
|
||||
# 应用端口(容器内端口)
|
||||
PORT=8000
|
||||
# Docker映射到宿主机的端口(docker-compose使用)
|
||||
HOST_PORT=10001
|
||||
# ================================
|
||||
对于 Docker Compose 部署,当前主要端口配置来源于:
|
||||
|
||||
- [docker-compose.yml](../../docker-compose.yml)(unified,默认入口)
|
||||
- [docker-compose.moldinsight.yml](../../docker-compose.moldinsight.yml)
|
||||
- [docker-compose.inventory.yml](../../docker-compose.inventory.yml)
|
||||
- `.env` / `deploy/.env.example`
|
||||
|
||||
注意:这些 compose 文件仅负责项目应用容器,不负责 PostgreSQL / Redis / 对象存储容器。
|
||||
|
||||
核心环境变量:
|
||||
|
||||
```env
|
||||
FRONTEND_PORT=10003 # unified 模式浏览器入口(前端 Nginx 对外)
|
||||
MOLDINSIGHT_PORT=10003 # moldinsight-only 独立部署时使用
|
||||
INVENTORY_PORT=10004 # inventory-only 独立部署时使用
|
||||
# 注意:unified backend 不暴露宿主机端口,仅经前端 /api 反代(docker 网络内部 backend:8000 互通)
|
||||
```
|
||||
|
||||
### 端口含义
|
||||
其余基础设施通常为:
|
||||
|
||||
| 变量 | 用途 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `PORT` | 应用监听端口 | 8000 | FastAPI/Uvicorn 服务监听的端口 |
|
||||
| `HOST_PORT` | 宿主机映射端口 | 10001 | Docker Compose 映射到宿主机的端口 |
|
||||
|
||||
### 使用场景
|
||||
|
||||
#### 1. 本地直接运行(Python)
|
||||
```bash
|
||||
python src/main.py
|
||||
```env
|
||||
DB_PORT=5432
|
||||
REDIS_PORT=6379
|
||||
```
|
||||
服务将在 `http://localhost:8000` 启动(使用 `PORT` 配置)
|
||||
|
||||
#### 2. Docker Compose 运行
|
||||
```bash
|
||||
docker-compose up
|
||||
对象存储常见端口(由服务器既有服务提供):
|
||||
|
||||
```env
|
||||
RUSTFS_ENDPOINT=http://localhost:9000
|
||||
```
|
||||
服务将在 `http://localhost:10001` 访问(使用 `HOST_PORT` 配置)
|
||||
容器内部使用 `PORT` 配置的端口(8000)
|
||||
|
||||
#### 3. 修改端口
|
||||
---
|
||||
|
||||
**场景A:只想修改外部访问端口(Docker)**
|
||||
```bash
|
||||
# .env 文件
|
||||
PORT=8000 # 容器内不变
|
||||
HOST_PORT=8080 # 宿主机改为8080
|
||||
## 2. 各端口的含义
|
||||
|
||||
| 变量 / 端口 | 用途 |
|
||||
|---|---|
|
||||
| `FRONTEND_PORT`(默认 10003) | 前端 Nginx 宿主机暴露端口(浏览器入口) |
|
||||
| unified backend | **不暴露宿主机端口**——经前端 /api 反代,docker 网络内 `backend:8000` 互通 |
|
||||
| `MOLDINSIGHT_PORT`(默认 10003) | moldinsight-only 独立部署端口 |
|
||||
| `INVENTORY_PORT`(默认 10004) | inventory-only 独立部署端口 |
|
||||
| `DB_PORT` | PostgreSQL 端口 |
|
||||
| `REDIS_PORT` | Redis 端口 |
|
||||
| `9000` | MinIO/RustFS S3 兼容 API |
|
||||
| `9001` | MinIO 控制台 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 推荐配置方式
|
||||
|
||||
### 3.1 moldinsight-only
|
||||
|
||||
```env
|
||||
MOLDINSIGHT_PORT=8000
|
||||
DB_PORT=5432
|
||||
REDIS_PORT=6379
|
||||
```
|
||||
访问地址:`http://localhost:8080`
|
||||
|
||||
**场景B:修改应用端口(容器内/本地运行)**
|
||||
```bash
|
||||
# .env 文件
|
||||
PORT=9000 # 应用改为9000
|
||||
HOST_PORT=10001 # 宿主机映射到10001
|
||||
### 3.2 inventory-only
|
||||
|
||||
```env
|
||||
INVENTORY_PORT=8001
|
||||
DB_PORT=5432
|
||||
REDIS_PORT=6379
|
||||
```
|
||||
- 本地运行:`http://localhost:9000`
|
||||
- Docker运行:`http://localhost:10001` (映射到容器内9000)
|
||||
|
||||
**场景C:同时修改两个端口**
|
||||
```bash
|
||||
# .env 文件
|
||||
PORT=9000
|
||||
HOST_PORT=9000
|
||||
### 3.3 full / 模块同时部署
|
||||
|
||||
```env
|
||||
MOLDINSIGHT_PORT=8000
|
||||
INVENTORY_PORT=8001
|
||||
DB_PORT=5432
|
||||
REDIS_PORT=6379
|
||||
```
|
||||
- 本地运行:`http://localhost:9000`
|
||||
- Docker运行:`http://localhost:9000`
|
||||
|
||||
## 配置文件说明
|
||||
---
|
||||
|
||||
### 配置读取优先级
|
||||
## 4. direct run 与 Compose 的映射
|
||||
|
||||
1. **`config/settings.py`** - 从 `.env` 读取 `PORT` 和 `HOST`
|
||||
2. **`src/main.py`** - 从 `settings` 获取端口配置
|
||||
3. **`docker-compose.yml`** - 从 `.env` 读取 `HOST_PORT` 和 `PORT`
|
||||
通过直接运行或 Docker Compose 部署时,端口含义保持一致,但映射方式不同。
|
||||
|
||||
### 相关文件
|
||||
### direct run
|
||||
|
||||
- **`.env`** - ⭐ 唯一需要修改的配置文件
|
||||
- **`config/settings.py`** - 配置读取逻辑(无需修改)
|
||||
- **`src/main.py`** - 使用配置启动服务(无需修改)
|
||||
- **`docker-compose.yml`** - Docker端口映射(自动读取 `.env`)
|
||||
- **`start.sh` / `start_fixed.sh`** - 启动脚本(自动读取 `.env`)
|
||||
moldinsight:
|
||||
|
||||
## 常见问题
|
||||
```bash
|
||||
uvicorn src.entrypoints.moldinsight:app --port 8000
|
||||
```
|
||||
|
||||
### Q: 为什么 Docker 宿主机端口和应用端口分开配置?
|
||||
A: 这样可以灵活调整容器端口而不影响外部访问,也避免端口冲突。
|
||||
inventory:
|
||||
|
||||
### Q: 修改后需要重启吗?
|
||||
A: 是的,修改 `.env` 后需要重启服务才能生效:
|
||||
- 本地运行:Ctrl+C 停止后重新 `python src/main.py`
|
||||
- Docker: `docker-compose down && docker-compose up`
|
||||
```bash
|
||||
uvicorn src.entrypoints.inventory:app --port 8001
|
||||
```
|
||||
|
||||
### Q: 如何避免端口冲突?
|
||||
A: 确保 `HOST_PORT` 不与其他服务冲突,可以使用 `netstat -an | grep <端口>` 检查端口占用情况。
|
||||
### Docker Compose
|
||||
|
||||
### Q: 可以使用 80 端口吗?
|
||||
A: 可以,但需要管理员权限:
|
||||
- Linux/Mac: 使用 sudo
|
||||
- Docker: 需要容器有足够权限
|
||||
- 生产环境建议使用反向代理(如 Nginx)
|
||||
Compose 通过端口映射暴露服务:
|
||||
|
||||
- moldinsight → `${MOLDINSIGHT_PORT}:8000`
|
||||
- inventory → `${INVENTORY_PORT}:8001`
|
||||
|
||||
当前实际定义见:
|
||||
- [docker-compose.yml](../../docker-compose.yml)(unified / moldinsight-only)
|
||||
- [docker-compose.inventory.yml](../../docker-compose.inventory.yml)(inventory-only 的 `INVENTORY_PORT`)
|
||||
|
||||
---
|
||||
|
||||
## 5. 与前端配置的关系
|
||||
|
||||
前端是否使用 unified / split deployment,会影响前端 API 地址配置。
|
||||
|
||||
### unified
|
||||
- 一个 API 基地址
|
||||
|
||||
### split
|
||||
- moldinsight 与 inventory 各自基地址
|
||||
|
||||
因此,修改后端端口后,可能还需要同步:
|
||||
|
||||
- 前端 `.env`
|
||||
- Nginx 反向代理
|
||||
- 浏览器访问地址
|
||||
|
||||
---
|
||||
|
||||
## 6. 推荐实践
|
||||
|
||||
1. **本地开发**
|
||||
- moldinsight:8000
|
||||
- inventory:8001
|
||||
|
||||
2. **服务器部署**
|
||||
- 外网只暴露 80/443
|
||||
- Nginx 反代到 8000 / 8001
|
||||
|
||||
3. 修改端口后,同步检查 `.env`、Compose 端口映射、前端环境变量与反向代理配置
|
||||
|
||||
---
|
||||
|
||||
## 7. 关联文档
|
||||
|
||||
- [LINUX_SETUP.md](./LINUX_SETUP.md)
|
||||
- [DEPLOY_PORT.md](./DEPLOY_PORT.md)
|
||||
- [archive/BACKEND_MODULARIZATION_BLUEPRINT.md](../archive/BACKEND_MODULARIZATION_BLUEPRINT.md)
|
||||
- [README.md](../../README.md)
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
# 端口配置重构总结
|
||||
|
||||
## 修改内容
|
||||
|
||||
本次重构将项目的端口配置统一到 `.env` 文件中,确保整个项目只有一个地方需要修改端口。
|
||||
|
||||
## 修改的文件
|
||||
|
||||
### 1. ⭐ `.env` - 唯一配置入口
|
||||
**变更:** 添加了统一的端口配置区域
|
||||
```bash
|
||||
# ================================
|
||||
# 端口配置 - 唯一修改端口的地方
|
||||
# ================================
|
||||
# 应用端口(容器内端口)
|
||||
PORT=8000
|
||||
# Docker映射到宿主机的端口(docker-compose使用)
|
||||
HOST_PORT=10001
|
||||
# ================================
|
||||
```
|
||||
|
||||
### 2. `src/main.py`
|
||||
**变更:** 从硬编码的环境变量读取改为从 `config.settings` 读取
|
||||
```python
|
||||
# 修改前
|
||||
host = os.getenv('HOST', '0.0.0.0')
|
||||
port = int(os.getenv('PORT', '8000'))
|
||||
|
||||
# 修改后
|
||||
from config.settings import settings
|
||||
# ...
|
||||
host=settings.HOST,
|
||||
port=settings.PORT
|
||||
```
|
||||
|
||||
### 3. `docker-compose.yml`
|
||||
**变更:** 端口映射从硬编码改为从环境变量读取
|
||||
```yaml
|
||||
# 修改前
|
||||
ports:
|
||||
- "10001:8000"
|
||||
environment:
|
||||
- PORT=8000
|
||||
|
||||
# 修改后
|
||||
ports:
|
||||
- "${HOST_PORT:-10001}:${CONTAINER_PORT:-8000}"
|
||||
environment:
|
||||
- HOST=${HOST:-0.0.0.0}
|
||||
- PORT=${CONTAINER_PORT:-8000}
|
||||
```
|
||||
|
||||
### 4. `start.sh` 和 `start_fixed.sh`
|
||||
**变更:** 自动从 `.env` 读取端口并显示正确的访问地址
|
||||
```bash
|
||||
# 添加
|
||||
PORT=$(grep '^PORT=' .env 2>/dev/null | cut -d'=' -f2 || echo '8000')
|
||||
echo "🌐 服务将在 http://localhost:${PORT} 启动"
|
||||
```
|
||||
|
||||
### 5. `README.md`
|
||||
**变更:** 更新访问说明,提示端口配置位置
|
||||
|
||||
### 6. 新增文件
|
||||
- `.env.example` - 配置文件模板
|
||||
- `PORT_CONFIG.md` - 端口配置详细说明
|
||||
- `DEPLOY_PORT.md` - 部署时端口配置指南
|
||||
|
||||
## 配置读取流程
|
||||
|
||||
```
|
||||
.env 文件
|
||||
↓
|
||||
config/settings.py (读取 PORT 和 HOST)
|
||||
↓
|
||||
src/main.py (使用 settings.PORT)
|
||||
↓
|
||||
uvicorn 启动服务
|
||||
```
|
||||
|
||||
Docker 部署流程:
|
||||
```
|
||||
.env 文件
|
||||
↓
|
||||
docker-compose.yml (读取 HOST_PORT 和 PORT)
|
||||
↓
|
||||
容器映射和内部启动
|
||||
```
|
||||
|
||||
## 如何修改端口
|
||||
|
||||
### 方法 1:修改应用端口
|
||||
```bash
|
||||
# 编辑 .env
|
||||
PORT=9000 # 修改此行
|
||||
```
|
||||
- 本地运行:`http://localhost:9000`
|
||||
- Docker运行:需同时修改 `HOST_PORT=9000`
|
||||
|
||||
### 方法 2:修改 Docker 外部访问端口
|
||||
```bash
|
||||
# 编辑 .env
|
||||
HOST_PORT=8080 # 修改此行(PORT 保持不变)
|
||||
```
|
||||
- Docker运行:`http://localhost:8080`
|
||||
- 容器内仍使用 PORT 配置的端口
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 测试 1:本地运行
|
||||
```bash
|
||||
# 修改 .env 中的 PORT
|
||||
PORT=9999
|
||||
|
||||
# 启动服务
|
||||
python src/main.py
|
||||
|
||||
# 验证
|
||||
curl http://localhost:9999/health
|
||||
```
|
||||
|
||||
### 测试 2:Docker 运行
|
||||
```bash
|
||||
# 修改 .env
|
||||
PORT=8000
|
||||
HOST_PORT=9999
|
||||
|
||||
# 启动容器
|
||||
docker-compose up -d
|
||||
|
||||
# 验证
|
||||
curl http://localhost:9999/health
|
||||
```
|
||||
|
||||
### 测试 3:启动脚本
|
||||
```bash
|
||||
# 修改 .env
|
||||
PORT=8888
|
||||
|
||||
# 运行启动脚本
|
||||
./start.sh
|
||||
|
||||
# 检查输出是否显示正确的端口
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. ✅ 所有端口配置集中在 `.env` 文件
|
||||
2. ✅ 无需修改代码文件即可更改端口
|
||||
3. ✅ 支持本地运行和 Docker 部署两种场景
|
||||
4. ✅ 提供了详细的配置文档
|
||||
5. ⚠️ 修改端口后需要重启服务
|
||||
6. ⚠️ Docker 部署时需要同时考虑容器内外端口
|
||||
7. ⚠️ 确保新端口没有被占用
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 修改的文件
|
||||
- `.env` - 添加端口配置区域
|
||||
- `src/main.py` - 统一使用 settings 配置
|
||||
- `docker-compose.yml` - 支持环境变量配置端口
|
||||
- `start.sh` - 自动读取和显示端口
|
||||
- `start_fixed.sh` - 自动读取和显示端口
|
||||
- `README.md` - 更新访问说明
|
||||
|
||||
### 新增的文件
|
||||
- `.env.example` - 配置模板
|
||||
- `PORT_CONFIG.md` - 端口配置详细说明
|
||||
- `DEPLOY_PORT.md` - 部署配置指南
|
||||
- `PORT_REFACTOR_SUMMARY.md` - 本文档
|
||||
|
||||
## 回滚方案
|
||||
|
||||
如果需要回滚,按以下步骤操作:
|
||||
|
||||
```bash
|
||||
git checkout -- src/main.py
|
||||
git checkout -- docker-compose.yml
|
||||
git checkout -- start.sh start_fixed.sh
|
||||
git checkout -- .env
|
||||
git checkout -- README.md
|
||||
|
||||
# 删除新增文件
|
||||
rm .env.example PORT_CONFIG.md DEPLOY_PORT.md PORT_REFACTOR_SUMMARY.md
|
||||
```
|
||||
|
||||
## 联系支持
|
||||
|
||||
如有问题,请查看:
|
||||
- `PORT_CONFIG.md` - 端口配置详细说明
|
||||
- `DEPLOY_PORT.md` - 部署配置指南
|
||||
- `.env.example` - 配置示例
|
||||
+3
@@ -1,5 +1,8 @@
|
||||
# 外部接口与集成盘点模板(ERP/财务/供应商门户/客户门户)
|
||||
|
||||
> 文档定位:**接口梳理与集成盘点模板文档**。
|
||||
> 本文是用于外部系统对接时的模板,不作为当前项目状态或当前接口实现清单的权威说明。当前状态见 [../STATUS.md](../STATUS.md),当前架构见 [../ARCHITECTURE.md](../ARCHITECTURE.md)。
|
||||
|
||||
## 1. 接口清单
|
||||
|
||||
| 接口名称 | 调用方向 | 协议 | 鉴权 | 频率 | 单次数据量 | 幂等键 | 超时 | 重试策略 | 死信/补偿 | 负责人 |
|
||||
@@ -1,5 +1,8 @@
|
||||
# UAT 验收清单(模具制造进销存主线)
|
||||
|
||||
> 文档定位:**UAT 验收模板 / 验收过程文档**。
|
||||
> 本文用于业务验收与签字过程,不作为当前项目状态或当前架构的权威说明。当前状态见 [../STATUS.md](../STATUS.md),当前架构见 [../ARCHITECTURE.md](../ARCHITECTURE.md)。
|
||||
|
||||
## 1. 业务流程签字
|
||||
|
||||
| 模块 | 场景 | 验收点 | 结果 | 业务签字 | 日期 |
|
||||
@@ -0,0 +1,86 @@
|
||||
# OCC 处理吞吐与隔离方案设计(OCC_THROUGHPUT)
|
||||
|
||||
> 文档定位:**OCC(PythonOCC)处理吞吐与故障隔离的专题设计文档**。
|
||||
> 本文回答"OCC 串行瓶颈与超时线程泄漏的根治路线";现状事实以 [../../../STATUS.md](../../../STATUS.md) 为准,债务归属 [../../TECH_DEBT.md](../../TECH_DEBT.md) D10。
|
||||
> 2026-09-17 随批次 4 产出:**方案设计先行**,短期项(方案 A)零代码可用,中期的接口演进与进程池实施留待后续批次。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状与硬约束
|
||||
|
||||
### 1.1 运行时事实
|
||||
|
||||
- 所有 OCC 操作(STEP 解析 / 布尔运算 / 三角化 / 倒扣检测 / STEP 转换等)统一经 [processing_service.py](../../../src/moldinsight/services/processing_service.py) 的 `run_occ(op_name, payload)` 投入 **常驻 OCC 工作进程池**([occ_process_pool.py](../../../src/moldinsight/services/occ_process_pool.py),默认 1 进程 = 1 串行通道)——OCC 非线程安全,通道内串行是正确性要求,不是实现偷懒。
|
||||
- 操作在 [occ_worker.py](../../../src/moldinsight/core/occ_worker.py) 以注册表形式实现(parse_stp / generate_mesh / generate_cavity / analyze_mold_design / detect_undercuts / convert_component_step 等);输入输出全部是**文件路径 + 普通字典**,TopoDS_Shape 不跨进程传输(方案 B 硬约束,见 §1.2)。
|
||||
- Celery worker 为 prefork 模式,`processing_service` 是模块级单例:**每个 worker 子进程各持一个常驻 OCC 工作进程**。因此 OCC 并行度 = worker 子进程数,与 web 进程数无关(web 侧 `run_occ` 仅服务于轻量同步调用,如倒扣检测)。
|
||||
- 超时/崩溃 = `terminate()` 该工作进程并换新补位——进程边界干净回收,无线程滞留。
|
||||
|
||||
### 1.2 硬约束(决定方案边界)
|
||||
|
||||
| 约束 | 含义 |
|
||||
|---|---|
|
||||
| OCC 非线程安全 | 任何方案中,**一个进程内 OCC 操作必须串行**;并行只能靠多进程 |
|
||||
| C++ 栈不可中断 | 线程级超时只能"抛弃"不能"击杀";**只有进程级 kill 是干净的故障恢复** |
|
||||
| `run_occ(fn, *args)` 传闭包/绑定方法 | 函数对象不可跨进程 pickle——进程化方案必须改接口为"操作名 + 可序列化参数" |
|
||||
| STEP 重载成本 | 进程间不共享 OCC 形状对象;跨进程方案每次调用需重新读文件/传 BRep(几秒级) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案对比
|
||||
|
||||
### 方案 A:Celery prefork 并行伸缩(短期,零新代码)
|
||||
|
||||
**做法**:承认"每子进程一个串行 OCC 通道"的既有事实,把 OCC 吞吐问题转化为 worker 进程数问题:`celery -A celery_app worker --concurrency=N`,N = 期望的并行分析数(受 CPU 核数与每进程内存约束)。
|
||||
|
||||
- **优点**:零代码改动;进程边界天然兜住线程泄漏——泄漏线程随子进程存亡,配合 `--max-tasks-per-child=M`(子进程处理 M 个任务后重启回收)可把滞留线程的存续时间限制在一个批次内。
|
||||
- **代价**:每个子进程常驻完整 Python + OCC 运行时(数百 MB),N 不能无脑调大;DB 连接按 celery 角色池(pool_size=5)随子进程倍增,PG `max_connections` 需要相应预算。
|
||||
- **不解决**:单任务超时后该子进程内的线程滞留(被 max-tasks-per-child 兜底回收);单任务无加速(串行本质不变)。
|
||||
|
||||
**结论:立即可用的推荐做法**。部署侧调整(concurrency / max-tasks-per-child)随下次镜像与 compose 评审落地,先在 [OPERATIONS.md](../../OPERATIONS.md) 记录启动参数建议。
|
||||
|
||||
### 方案 B:常驻 OCC 进程池 + kill-on-timeout(中期,推荐演进方向)
|
||||
|
||||
**做法**:在 `run_occ` 接口之下替换执行器——不再是 `ThreadPoolExecutor`,而是**常驻的单线程 OCC 工作进程池**(每进程一个事件循环:接任务 → 执行 → 回报)。超时由主进程 `terminate()` 工作进程并更换新进程补位。
|
||||
|
||||
- 接口演进:`run_occ(fn, *args)` → `run_occ(op_name: str, payload: dict)`,操作名注册表映射到模块级函数(STEP 文件路径进、JSON/BRep 文件出,杜绝 pickle 大对象);各调用点(解析、型腔、倒扣、导出三角化……)逐一迁移。
|
||||
- **优点**:超时 = 杀进程,**故障恢复干净彻底**(D10 残留泄漏根治);OCC 崩溃(segfault)不再波及 API/worker 主进程;进程池大小与 celery 并发解耦。
|
||||
- **代价**:一次明确的接口迁移(所有 `run_occ` 调用点 + 结果序列化);进程池自管理(补位、健康检查、启动预热——spawn 下 import OCC 秒级,需常驻而非按任务拉起);跨进程只传文件路径 + JSON,现有"传形状对象"的内部调用要改为落盘中转。
|
||||
- **风险**:自建进程池的运维复杂度;Windows 开发环境 spawn 语义与 Linux fork 差异需测试覆盖。
|
||||
|
||||
### 方案 C:OCC sidecar 服务(长期,视伸缩需求)
|
||||
|
||||
**做法**:OCC 能力独立成进程/容器(HTTP 或 gRPC),API 与 worker 都是客户端;STEP 按路径/对象键传入,返回 JSON 摘要 + 产物对象键。
|
||||
|
||||
- **优点**:隔离最彻底;OCC 可独立伸缩、独立发布、独立扩容 GPU/内存型节点;多语言可复用。
|
||||
- **代价**:新增一个部署单元与序列化边界(大网格/形状数据传输设计);超出当前"单 compose 栈"的部署叙事,需与 DEPLOYMENT 文档体系一起演进。
|
||||
|
||||
**结论:除非出现独立伸缩/隔离性硬需求,暂不启动。**
|
||||
|
||||
---
|
||||
|
||||
## 3. 决策与路线
|
||||
|
||||
| 阶段 | 动作 | 状态 |
|
||||
|---|---|---|
|
||||
| 短期 | 方案 A:`--concurrency` 伸缩 + `--max-tasks-per-child` 兜底回收;`cancel_futures=True` 修复重建并发风险 | ✅ 部署参数 2026-09-18 落地(`CELERY_CONCURRENCY` / `CELERY_MAX_TASKS_PER_CHILD` 进 compose + .env.example;2026-09-24 起 worker 与后端共用 `gemold-backend` 镜像、Dockerfile.celery 移除,参数以 compose `command:` 覆盖传递) |
|
||||
| 中期 | 方案 B:`run_occ(op_name, payload)` 接口演进 + 常驻进程池,kill-on-timeout 根治泄漏 | ✅ 2026-09-18 实施完成(见 §5;回归测试 [tests/test_occ_process_pool.py](../../../tests/test_occ_process_pool.py)) |
|
||||
| 长期 | 方案 C:sidecar,仅在出现独立伸缩需求时启动 | 暂不启动 |
|
||||
|
||||
## 4. 已落地的缓解(2026-09-17,批次 4)
|
||||
|
||||
`_reset_occ_executor` 的 `shutdown(wait=False)` 补 `cancel_futures=True`。这不只是卫生问题:旧实现下旧 executor 的**排队任务不会消失**——若挂死线程后来"慢恢复",旧线程会继续消化旧队列,与新 executor **并发操作非线程安全的 OCC**(数据竞争 / 崩溃风险)。补参后排队任务即被丢弃,残留问题收敛为"运行中线程滞留 1 个",由方案 A 的进程回收兜底。
|
||||
|
||||
## 5. 方案 B 实施记录(2026-09-18)
|
||||
|
||||
**接口**:`run_occ(fn, *args)` → `run_occ(op_name, payload)`;执行器由进程内线程池替换为常驻进程池。
|
||||
|
||||
- **新增** [occ_process_pool.py](../../../src/moldinsight/services/occ_process_pool.py):`OccProcessPool`(默认 size=1)。每个 `_OccWorker` 是一个 spawn 出的常驻子进程 + 双工管道 + 独立 `asyncio.Lock`(通道串行);阻塞收发经 `asyncio.to_thread` 不卡事件循环。操作超时或进程死亡 → `terminate()` + 换新补位;任务级整体超时(`process_file_with_storage` 外层 wait_for)→ `recover()` 整体重建。`shutdown()` 供应用退出/测试清理。
|
||||
- **新增** [occ_worker.py](../../../src/moldinsight/core/occ_worker.py):操作注册表 + `worker_main` 消息循环。OCC 模块在 handler 内惰性导入(父进程 pip 环境无 OCC 也可 import),进程内单例缓存(parser/planner/analyzer/mesh_gen 等)。全部操作输入输出为**文件路径 + 普通字典**,TopoDS 不跨进程。
|
||||
- **调用点迁移**(processing_service):
|
||||
- `parse_stp` = 原 load_step_file + analyze_geometry 两步合一(形状在子进程内即生即用)
|
||||
- `generate_mesh` / `analyze_mold_design` / `detect_undercuts` / `convert_component_step` 同名对位
|
||||
- `generate_cavity` = 分模 + 方案形状持久化 STEP 导出全在子进程内;`_export_shapes`(TopoDS)不再回主进程,返回 export_manifest(与旧 `_persist_step_exports` 结构一致,主进程原样存 export_artifacts)
|
||||
- 旧 `_cache_export_shapes` / `get_export_shapes` / `_persist_step_exports` / `_export_shapes_cache` 及线程 executor / `_reset_occ_executor` 全部删除(跨进程本就不存在内存形状缓存,export_router 相应移除 `export_mold_results` 内存分支)
|
||||
- [shape_loader.py](../../../src/moldinsight/services/shape_loader.py) → [stp_materializer.py](../../../src/moldinsight/services/stp_materializer.py):只把 STP 原件落盘临时文件,OCC 解析交给子进程操作
|
||||
- **成本确认**:每个 spawn 子进程首次操作需 import OCC(秒级);进程常驻后后续操作复用缓存实例。每个操作从 STP 原件重新加载形状(STEP 重载成本,见 §1.2)——原线程方案跨步骤共享 shape 的内存优势让位于进程隔离,符合方案 B 设计取舍。
|
||||
- **测试**:[tests/test_occ_process_pool.py](../../../tests/test_occ_process_pool.py)(OCC-gated,6 例):spawn+管道往返 / 未知操作错误回传 / 子进程异常浮出 / 超时换新补位 / 真实盒体 STP 解析端到端 / generate_cavity 分模+STEP 落盘端到端。
|
||||
@@ -1,5 +1,7 @@
|
||||
# RustFS 对象存储集成说明
|
||||
|
||||
> 文档定位:**RustFS / 对象存储集成的专题说明文档**。
|
||||
> 本文解释对象存储侧的接口与集成思路,不作为当前部署入口文档。当前部署方式见 [../../DEPLOYMENT.md](../../DEPLOYMENT.md) 与 [../../deployment/LINUX_SETUP.md](../../deployment/LINUX_SETUP.md),当前状态见 [../../STATUS.md](../../STATUS.md)。
|
||||
## 架构概述
|
||||
|
||||
本项目采用 **RustFS** 作为对象存储和 **PostgreSQL** 作为元数据存储的双层存储架构。
|
||||
@@ -0,0 +1,158 @@
|
||||
# 存储架构说明
|
||||
|
||||
> 文档定位:**存储分层与数据流的专题说明文档**。
|
||||
> 本文主要解释 geMoldInsight 的存储架构,不作为当前部署入口或当前状态的权威说明。当前部署方式见 [../../DEPLOYMENT.md](../../DEPLOYMENT.md),当前状态见 [../../STATUS.md](../../STATUS.md),总体架构见 [../../ARCHITECTURE.md](../../ARCHITECTURE.md)。
|
||||
> 其历史中的“本项目自行拉起 PostgreSQL / MinIO 并通过 `python src/main.py` 启动单体”的部分,**已不再代表当前默认部署方式**。
|
||||
|
||||
当前默认部署前提是:
|
||||
|
||||
- PostgreSQL 由服务器已有服务提供
|
||||
- Redis 由服务器已有服务提供
|
||||
- RustFS / MinIO 兼容对象存储由服务器已有服务提供
|
||||
- 项目自身只部署:`moldinsight` / `moldinsight-celery` / `inventory`
|
||||
|
||||
如需查看当前部署方式,请优先参考:
|
||||
- [../../../README.md](../../../README.md)
|
||||
- [../../deployment/LINUX_SETUP.md](../../deployment/LINUX_SETUP.md)
|
||||
- [../../deployment/DEPLOY_PORT.md](../../deployment/DEPLOY_PORT.md)
|
||||
- [../../archive/BACKEND_MODULARIZATION_BLUEPRINT.md](../../archive/BACKEND_MODULARIZATION_BLUEPRINT.md)
|
||||
|
||||
---
|
||||
|
||||
## 架构概述
|
||||
|
||||
本项目采用 **RustFS(S3兼容)对象存储** + **PostgreSQL 元数据存储** 的双层存储架构。
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 应用层 (moldinsight / inventory) │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
┌───────▼────────┐ ┌─────────▼─────────┐
|
||||
│ PostgreSQL │ │ RustFS / S3 │
|
||||
│ (元数据) │ │ (对象存储) │
|
||||
│ │ │ │
|
||||
│ - users │ │ - stp-files │
|
||||
│ - stp_files │ │ - geometry │
|
||||
│ - geometry_data│ │ - mold-cavities │
|
||||
│ - processing │ │ - html-files │
|
||||
│ - logs │ │ - user-files │
|
||||
└────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PostgreSQL 数据表(摘要)
|
||||
|
||||
### 用户管理
|
||||
- `users` - 用户信息
|
||||
- `roles` / `permissions` - 权限体系
|
||||
|
||||
### moldinsight 相关
|
||||
- `stp_files` - STP 文件元数据
|
||||
- `html_files` - HTML 报告元数据
|
||||
- `geometry_data` - 几何分析数据
|
||||
- `mold_cavity_data` - 模具型腔数据
|
||||
- `feature_detections` - 特征检测结果
|
||||
- `design_recommendations` - 设计建议
|
||||
- `processing_tasks` - 处理任务记录
|
||||
- `analysis_metrics` - 分析指标
|
||||
|
||||
### inventory 相关
|
||||
- `products` - 产品/物料
|
||||
- `product_materials` - BOM
|
||||
- `inventory` - 库存
|
||||
- `stock_movements` - 库存流水
|
||||
- `purchase_orders` / `sales_orders` - 订单
|
||||
- `finance_transactions` - 财务流水
|
||||
|
||||
---
|
||||
|
||||
## RustFS / S3 存储桶
|
||||
|
||||
| 存储桶名称 | 用途 | 存储内容 |
|
||||
|---|---|---|
|
||||
| `moldinsight-stp-files` | STP/STEP 文件 | 用户上传的原始 3D 模型 |
|
||||
| `moldinsight-geometry` | 几何结果 | 几何分析 JSON |
|
||||
| `moldinsight-mold-cavities` | 模具结果 | 模具设计 JSON |
|
||||
| `moldinsight-html` | HTML 报告 | 生成的 HTML 报告文件 |
|
||||
| `moldinsight-user-files` | 用户文件 | 其他附件/用户文件 |
|
||||
|
||||
---
|
||||
|
||||
## 当前推荐初始化方式
|
||||
|
||||
### 1. 准备环境变量
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
确保以下变量指向**服务器上已存在的真实服务**:
|
||||
|
||||
```env
|
||||
DB_HOST=your-db-host
|
||||
DB_PORT=5432
|
||||
DB_NAME=moldinsight
|
||||
DB_USER=moldinsight
|
||||
DB_PASSWORD=change-me
|
||||
|
||||
REDIS_HOST=your-redis-host
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
RUSTFS_ENDPOINT=http://your-storage-host:9000
|
||||
RUSTFS_ACCESS_KEY=change-me
|
||||
RUSTFS_SECRET_KEY=change-me
|
||||
```
|
||||
|
||||
### 2. 初始化数据库
|
||||
|
||||
当前初始化入口参考:
|
||||
- [init_db.py](../../../src/shared/database/init_db.py)
|
||||
|
||||
### 3. 启动项目服务
|
||||
|
||||
当前推荐通过(按模式对应不同 compose 文件):
|
||||
- `docker compose up -d`([docker-compose.yml](../../../docker-compose.yml),默认 unified)
|
||||
- `docker compose -f docker-compose.moldinsight.yml up -d`
|
||||
- `docker compose -f docker-compose.inventory.yml up -d`
|
||||
- 或直接 [src/entrypoints/](../../../src/entrypoints/) 入口
|
||||
|
||||
而不是继续使用历史单体 `python src/main.py` 作为默认方式。
|
||||
|
||||
---
|
||||
|
||||
## 监控与维护
|
||||
|
||||
### 对象存储
|
||||
- 检查对象存储服务可达性
|
||||
- 定期清理历史产物
|
||||
- 配置生命周期策略
|
||||
|
||||
### PostgreSQL
|
||||
- 定期备份
|
||||
- 监控连接池与慢查询
|
||||
- 保持 Alembic 迁移链一致
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 对象存储连接失败
|
||||
- 检查 `RUSTFS_ENDPOINT`
|
||||
- 检查 access key / secret key
|
||||
- 检查服务端口与网络策略
|
||||
|
||||
### 数据库连接失败
|
||||
- 检查 `DB_HOST` / `DB_PORT`
|
||||
- 检查数据库账号密码
|
||||
- 检查防火墙与白名单
|
||||
|
||||
### 文件上传失败
|
||||
- 检查对象存储可用性
|
||||
- 检查 Celery worker 是否运行
|
||||
- 检查 Redis 是否可达
|
||||
+68
-3
@@ -1,5 +1,70 @@
|
||||
# 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 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
这是 geMoldInsight 的独立前端工程,基于:
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
- Vue 3
|
||||
- Vite
|
||||
- TypeScript
|
||||
- Pinia
|
||||
- Vue Router
|
||||
|
||||
当前推荐部署方式为:
|
||||
|
||||
> **独立前端部署 + 同域反代 + unified backend**
|
||||
|
||||
即:
|
||||
- 前端由 Nginx 静态站点独立提供
|
||||
- `/api`、`/health`、`/html` 通过同域反代统一转发到一个 unified backend
|
||||
- 前端继续使用相对路径调用后端接口
|
||||
|
||||
---
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
开发服务器默认:
|
||||
- 端口:`5173`
|
||||
- 已代理:`/api`、`/health`、`/html`
|
||||
|
||||
---
|
||||
|
||||
## 生产构建
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
构建产物输出到:
|
||||
- `frontend/dist/`
|
||||
|
||||
该目录由前端 Nginx 镜像或外部静态站点托管,不再输出到仓库根目录 `static/`。
|
||||
|
||||
---
|
||||
|
||||
## 部署
|
||||
|
||||
当前仓库已提供:
|
||||
- [deploy/Dockerfile.frontend](../deploy/Dockerfile.frontend)
|
||||
- [deploy/nginx/frontend.conf](../deploy/nginx/frontend.conf)
|
||||
|
||||
以及根目录 [docker-compose.yml](../docker-compose.yml)(unified 模式)中的 `frontend` 服务。完整系统一键启动:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> 旧 `--profile frontend` 仅起前端的写法已随 compose 拆分移除;前端同域反代依赖 unified backend,推荐整栈启动。前后端分离开发时,前端本地 `npm run dev`、后端直跑 `uvicorn`(见 [LINUX_SETUP.md](../docs/deployment/LINUX_SETUP.md) §6)。
|
||||
|
||||
---
|
||||
|
||||
## 说明
|
||||
|
||||
前端历史上曾通过后端 `static/` 目录托管;当前已切换为独立部署模式。后端默认不再负责提供 SPA 页面,但仍提供:
|
||||
|
||||
- `/api/*`
|
||||
- `/health`
|
||||
- `/html/*`(gemold 分析产物)
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"gen:api": "npx --yes openapi-typescript@7.13.0 ../openapi.json -o src/types/api.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"air-datepicker": "^3.6.0",
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<div class="card aluminum-price-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<span class="aluminum-icon">🪙</span> 铝金属价格 (SHFE)
|
||||
<span class="aluminum-icon">🪙</span> 铝金属价格
|
||||
</div>
|
||||
<span class="aluminum-source">数据来源: 上海期货交易所 | 更新于 {{ aluminum.date }}</span>
|
||||
<span class="aluminum-source">{{ aluminumSourceLabel }}</span>
|
||||
</div>
|
||||
<div class="aluminum-content">
|
||||
<div class="aluminum-price-row">
|
||||
@@ -78,7 +78,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, nextTick, ref } from 'vue'
|
||||
import { computed, reactive, onMounted, nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { apiRequest } from '@/shared/api'
|
||||
import { Chart, registerables } from 'chart.js'
|
||||
@@ -98,6 +98,8 @@ interface AluminumPrice {
|
||||
low: number
|
||||
prev_close: number
|
||||
week_ago_price: number
|
||||
// D2:数据来源声明——simulated 为模拟走势(参考数据,非实时行情)
|
||||
source?: string
|
||||
}
|
||||
|
||||
interface HistoryItem {
|
||||
@@ -112,6 +114,7 @@ const aluminum = reactive({
|
||||
change: 0,
|
||||
change_percent: 0,
|
||||
open: 0,
|
||||
source: '',
|
||||
high: 0,
|
||||
low: 0,
|
||||
prev_close: 0,
|
||||
@@ -124,12 +127,21 @@ const formatAluminumPrice = (val: number | null) => {
|
||||
return Math.round(val).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// D2:来源标注以接口 source 字段为准,不再硬编码交易所名称——
|
||||
// 此前显示"数据来源: 上海期货交易所"而数据实为模拟走势,属虚假来源声明
|
||||
const aluminumSourceLabel = computed(() => {
|
||||
if (aluminum.source === 'simulated') return '模拟数据 · 参考走势,非实时行情'
|
||||
if (aluminum.source) return `数据来源: ${aluminum.source}`
|
||||
return `更新于 ${aluminum.date}`
|
||||
})
|
||||
|
||||
const loadAluminumPrice = async () => {
|
||||
try {
|
||||
const data = await apiRequest<AluminumPrice>('/api/aluminum-price/current')
|
||||
aluminum.price = data.price
|
||||
aluminum.unit = data.unit
|
||||
aluminum.date = data.date
|
||||
aluminum.source = data.source ?? ''
|
||||
aluminum.change = data.change
|
||||
aluminum.change_percent = data.change_percent
|
||||
aluminum.open = data.open
|
||||
|
||||
@@ -1,48 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { addNotification } from '@/shared/notification'
|
||||
import { useInventory } from '@/modules/inventory/composables/useInventory'
|
||||
import { useInventoryStore } from '@/stores/inventory'
|
||||
|
||||
import InventorySidebar from './components/InventorySidebar.vue'
|
||||
import DashboardTab from './components/DashboardTab.vue'
|
||||
import ProductsTab from './components/ProductsTab.vue'
|
||||
import MaterialsTab from './components/MaterialsTab.vue'
|
||||
import InventoryTab from './components/InventoryTab.vue'
|
||||
import PurchaseOrdersTab from './components/PurchaseOrdersTab.vue'
|
||||
import SalesOrdersTab from './components/SalesOrdersTab.vue'
|
||||
import SuppliersTab from './components/SuppliersTab.vue'
|
||||
import CustomersTab from './components/CustomersTab.vue'
|
||||
import FinanceTab from './components/FinanceTab.vue'
|
||||
import MovementsTab from './components/MovementsTab.vue'
|
||||
|
||||
const store = useAppStore()
|
||||
const store = useInventoryStore()
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
const {
|
||||
state,
|
||||
activeMenu,
|
||||
checkBackendHealth,
|
||||
loadDashboard,
|
||||
destroyPickers
|
||||
} = useInventory()
|
||||
const route = useRoute()
|
||||
|
||||
// Derive activeTab from route path (last segment)
|
||||
const tabFromRoute = () => {
|
||||
const segments = route.path.split('/')
|
||||
return segments[segments.length - 1] || 'dashboard'
|
||||
}
|
||||
|
||||
// Sync store.activeTab when route changes
|
||||
watch(() => route.path, () => {
|
||||
store.activeTab = tabFromRoute()
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
if (!store.user) {
|
||||
if (!appStore.user) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
checkBackendHealth().then(() => {
|
||||
if (!state.backendDbReady) {
|
||||
addNotification(state.backendDbMessage || '业务服务不可用', 'warning')
|
||||
store.activeTab = tabFromRoute()
|
||||
store.checkBackendHealth().then(() => {
|
||||
if (!store.backendDbReady) {
|
||||
addNotification(store.backendDbMessage || '业务服务不可用', 'warning')
|
||||
return
|
||||
}
|
||||
loadDashboard()
|
||||
store.loadDashboard()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
destroyPickers()
|
||||
store.destroyPickers()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -61,32 +58,23 @@ onUnmounted(() => {
|
||||
<section class="inventory-content">
|
||||
<div class="inventory-content-header">
|
||||
<div class="inventory-breadcrumb">
|
||||
<span>{{ activeMenu?.group?.title || '进销存' }}</span>
|
||||
<span>{{ store.activeMenu?.group?.title || '进销存' }}</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="current">{{ activeMenu?.item?.label || '' }}</span>
|
||||
<span class="current">{{ store.activeMenu?.item?.label || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t-alert
|
||||
v-if="!state.backendDbReady"
|
||||
:title="state.backendDbMessage"
|
||||
v-if="!store.backendDbReady"
|
||||
:title="store.backendDbMessage"
|
||||
theme="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
|
||||
<t-loading :loading="state.loading">
|
||||
<DashboardTab v-if="state.activeTab === 'dashboard'" />
|
||||
<ProductsTab v-else-if="state.activeTab === 'products'" />
|
||||
<MaterialsTab v-else-if="state.activeTab === 'materials'" />
|
||||
<InventoryTab v-else-if="state.activeTab === 'inventory'" />
|
||||
<PurchaseOrdersTab v-else-if="state.activeTab === 'purchases'" />
|
||||
<SalesOrdersTab v-else-if="state.activeTab === 'sales_orders'" />
|
||||
<SuppliersTab v-else-if="state.activeTab === 'suppliers'" />
|
||||
<CustomersTab v-else-if="state.activeTab === 'customers'" />
|
||||
<FinanceTab v-else-if="state.activeTab === 'finance'" />
|
||||
<MovementsTab v-else-if="state.activeTab === 'movements'" />
|
||||
<t-loading :loading="store.loading">
|
||||
<router-view />
|
||||
</t-loading>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -37,22 +37,22 @@ onMounted(() => {
|
||||
refreshFinance()
|
||||
})
|
||||
|
||||
// 字段名对齐后端 FinanceSummaryResponse:
|
||||
// receivable_total / payable_total / period_receipt_total / period_payment_total
|
||||
// 字段名对齐后端 FinanceSummaryResponse。
|
||||
// 后端 Decimal 序列化为 JSON 字符串,故用 Number() 转换再填充 0。
|
||||
const totalReceivable = computed(() => {
|
||||
return state.financeSummary?.receivable_total ?? 0
|
||||
return Number(state.financeSummary?.receivable_total) || 0
|
||||
})
|
||||
|
||||
const totalPayable = computed(() => {
|
||||
return state.financeSummary?.payable_total ?? 0
|
||||
return Number(state.financeSummary?.payable_total) || 0
|
||||
})
|
||||
|
||||
const periodReceived = computed(() => {
|
||||
return state.financeSummary?.period_receipt_total ?? 0
|
||||
return Number(state.financeSummary?.period_receipt_total) || 0
|
||||
})
|
||||
|
||||
const periodPaid = computed(() => {
|
||||
return state.financeSummary?.period_payment_total ?? 0
|
||||
return Number(state.financeSummary?.period_payment_total) || 0
|
||||
})
|
||||
|
||||
function getTransactionTypeLabel(type: string): string {
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useInventory } from '@/modules/inventory/composables/useInventory'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useInventoryStore } from '@/stores/inventory'
|
||||
|
||||
const { state, menuGroups, openGroups, toggleGroup, handleMenuClick } = useInventory()
|
||||
const router = useRouter()
|
||||
const store = useInventoryStore()
|
||||
const { menuGroups, openGroups, toggleGroup } = store
|
||||
|
||||
const navigateTo = (itemKey: string) => {
|
||||
store.activeTab = itemKey
|
||||
router.push(`/inventory/${itemKey}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -15,8 +23,8 @@ const { state, menuGroups, openGroups, toggleGroup, handleMenuClick } = useInven
|
||||
<div
|
||||
v-for="item in group.items"
|
||||
:key="item.key"
|
||||
:class="['sidebar-item', { active: state.activeTab === item.key }]"
|
||||
@click="handleMenuClick(item.key)"
|
||||
:class="['sidebar-item', { active: store.activeTab === item.key }]"
|
||||
@click="navigateTo(item.key)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
|
||||
@@ -98,16 +98,16 @@ async function openRestockModal() {
|
||||
}
|
||||
|
||||
function addRestockItem() {
|
||||
restockItems.value.push({
|
||||
restockItems.value = [...restockItems.value, {
|
||||
material_id: state.materials[0]?.id || null,
|
||||
quantity: 1,
|
||||
unit_price: 0,
|
||||
remark: ''
|
||||
})
|
||||
}]
|
||||
}
|
||||
|
||||
function removeRestockItem(index: number) {
|
||||
restockItems.value.splice(index, 1)
|
||||
restockItems.value = restockItems.value.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
const restockTotalAmount = computed(() => {
|
||||
|
||||
@@ -51,14 +51,14 @@ async function editProduct(product: any) {
|
||||
}
|
||||
|
||||
function addBomItem() {
|
||||
bomItems.value.push({
|
||||
bomItems.value = [...bomItems.value, {
|
||||
material_id: state.materials[0]?.id || null,
|
||||
quantity: 1
|
||||
})
|
||||
}]
|
||||
}
|
||||
|
||||
function removeBomItem(index: number) {
|
||||
bomItems.value.splice(index, 1)
|
||||
bomItems.value = bomItems.value.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
async function saveProduct() {
|
||||
|
||||
@@ -3,7 +3,9 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { DialogPlugin } from 'tdesign-vue-next'
|
||||
import { useInventory } from '../composables/useInventory'
|
||||
import { apiRequest } from '@/shared/api'
|
||||
import { inventoryApi } from '@/shared/api-client'
|
||||
import { addNotification, handleApiError } from '@/shared/notification'
|
||||
import type { Schema } from '@/types/schemas'
|
||||
|
||||
function confirmDialog(header: string, body: string, theme: string, confirmText: string, cancelText: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -47,6 +49,46 @@ const receiveWarehouseId = ref<number | null>(null)
|
||||
const receiveRemark = ref('')
|
||||
const receiving = ref(false)
|
||||
|
||||
// ── 采购需求推导 ──
|
||||
const showDemandModal = ref(false)
|
||||
const demandLoading = ref(false)
|
||||
const salesOrdersForDemand = ref<Schema<'SalesOrderResponse'>[]>([])
|
||||
const selectedSalesOrderIds = ref<number[]>([])
|
||||
const demandResult = ref<Schema<'PurchaseDemandResponse'> | null>(null)
|
||||
|
||||
async function openDemandDialog() {
|
||||
showDemandModal.value = true
|
||||
demandResult.value = null
|
||||
selectedSalesOrderIds.value = []
|
||||
try {
|
||||
const data = await inventoryApi.listSalesOrders({ limit: 100 })
|
||||
salesOrdersForDemand.value = (data as any)?.items || data || []
|
||||
} catch (e) {
|
||||
handleApiError(e, '加载销售订单')
|
||||
}
|
||||
}
|
||||
|
||||
async function calculateDemands() {
|
||||
if (!selectedSalesOrderIds.value.length) {
|
||||
addNotification('请选择至少一个销售订单', 'warning')
|
||||
return
|
||||
}
|
||||
demandLoading.value = true
|
||||
try {
|
||||
demandResult.value = await inventoryApi.calculatePurchaseDemands(selectedSalesOrderIds.value)
|
||||
} catch (e) {
|
||||
handleApiError(e, '计算采购需求')
|
||||
} finally {
|
||||
demandLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeDemandModal() {
|
||||
showDemandModal.value = false
|
||||
demandResult.value = null
|
||||
selectedSalesOrderIds.value = []
|
||||
}
|
||||
|
||||
function openCreateOrder() {
|
||||
editingItem.value = null
|
||||
form.value = {
|
||||
@@ -59,22 +101,22 @@ function openCreateOrder() {
|
||||
}
|
||||
|
||||
function addOrderItem() {
|
||||
orderItems.value.push({
|
||||
orderItems.value = [...orderItems.value, {
|
||||
product_id: state.materials[0]?.id || null,
|
||||
quantity: 1,
|
||||
remark: ''
|
||||
})
|
||||
}]
|
||||
}
|
||||
|
||||
function removeOrderItem(index: number) {
|
||||
orderItems.value.splice(index, 1)
|
||||
orderItems.value = orderItems.value.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
const orderTotalAmount = computed(() => {
|
||||
return orderItems.value.reduce((sum: number, item: any) => {
|
||||
const material = state.materials.find((m: any) => m.id === item.product_id)
|
||||
const unitPrice = material?.cost_price || 0
|
||||
return sum + unitPrice * (item.quantity || 0)
|
||||
const unitPrice = Number(material?.cost_price || 0)
|
||||
return sum + unitPrice * Number(item.quantity || 0)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
@@ -299,6 +341,7 @@ onMounted(() => {
|
||||
</t-select>
|
||||
<t-button @click="loadPurchaseOrders">刷新</t-button>
|
||||
<t-button type="primary" @click="openCreateOrder">新增采购订单</t-button>
|
||||
<t-button theme="success" variant="outline" @click="openDemandDialog">采购建议</t-button>
|
||||
</div>
|
||||
|
||||
<t-table :data="state.purchaseOrders" :loading="state.loading" stripe>
|
||||
@@ -448,7 +491,7 @@ onMounted(() => {
|
||||
</t-table-column>
|
||||
<t-table-column label="总价" width="120">
|
||||
<template #default="{ row: line }">
|
||||
{{ formatCurrency((state.materials.find((m: any) => m.id === line.product_id)?.cost_price || 0) * (line.quantity || 0)) }}
|
||||
{{ formatCurrency(Number(state.materials.find((m: any) => m.id === line.product_id)?.cost_price || 0) * Number(line.quantity || 0)) }}
|
||||
</template>
|
||||
</t-table-column>
|
||||
<t-table-column label="备注" width="150">
|
||||
@@ -515,6 +558,87 @@ onMounted(() => {
|
||||
<t-button type="primary" :loading="receiving" :disabled="receiving" @click="receivePurchaseOrder">确认入库</t-button>
|
||||
</template>
|
||||
</t-dialog>
|
||||
<!-- 采购需求推导对话框 -->
|
||||
<t-dialog
|
||||
v-model:visible="showDemandModal"
|
||||
title="采购需求推导"
|
||||
width="1000px"
|
||||
@closed="closeDemandModal"
|
||||
>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<p style="margin-bottom: 8px; color: var(--td-text-color-secondary);">
|
||||
选择销售订单,系统将自动按 BOM 展开物料需求、对比库存、推荐供应商。
|
||||
</p>
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
<t-select
|
||||
v-model="selectedSalesOrderIds"
|
||||
multiple
|
||||
placeholder="请选择销售订单"
|
||||
style="flex: 1;"
|
||||
:loading="salesOrdersForDemand.length === 0"
|
||||
>
|
||||
<t-option
|
||||
v-for="order in salesOrdersForDemand"
|
||||
:key="order.id"
|
||||
:label="`${order.order_no} - ${order.customer_name || ''}`"
|
||||
:value="order.id"
|
||||
/>
|
||||
</t-select>
|
||||
<t-button
|
||||
type="primary"
|
||||
:loading="demandLoading"
|
||||
:disabled="!selectedSalesOrderIds.length"
|
||||
@click="calculateDemands"
|
||||
>
|
||||
计算
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="demandResult">
|
||||
<div style="display: flex; gap: 24px; margin-bottom: 12px; font-weight: 600;">
|
||||
<span>来源订单:{{ demandResult.source_order_nos?.join(', ') || '-' }}</span>
|
||||
<span>缺货物料:<t-tag :type="demandResult.shortage_count > 0 ? 'danger' : 'success'" size="small">{{ demandResult.shortage_count }}</t-tag></span>
|
||||
<span>预计采购总额:{{ formatCurrency(Number(demandResult.total_estimated_cost || 0)) }}</span>
|
||||
</div>
|
||||
|
||||
<t-table :data="demandResult.items || []" stripe border size="small" max-height="400">
|
||||
<t-table-column prop="material_sku" label="物料SKU" width="120" />
|
||||
<t-table-column prop="material_name" label="物料名称" min-width="140" />
|
||||
<t-table-column label="需求量" width="90">
|
||||
<template #default="{ row }">{{ row.required_quantity }}</template>
|
||||
</t-table-column>
|
||||
<t-table-column label="库存量" width="90">
|
||||
<template #default="{ row }">{{ row.available_quantity }}</template>
|
||||
</t-table-column>
|
||||
<t-table-column label="缺口" width="90">
|
||||
<template #default="{ row }">
|
||||
<t-tag :type="row.shortage_quantity > 0 ? 'danger' : 'success'" size="small">
|
||||
{{ row.shortage_quantity }}
|
||||
</t-tag>
|
||||
</template>
|
||||
</t-table-column>
|
||||
<t-table-column label="单价" width="90">
|
||||
<template #default="{ row }">{{ formatCurrency(Number(row.unit_cost || 0)) }}</template>
|
||||
</t-table-column>
|
||||
<t-table-column label="预计金额" width="110">
|
||||
<template #default="{ row }">{{ formatCurrency(Number(row.estimated_cost || 0)) }}</template>
|
||||
</t-table-column>
|
||||
<t-table-column prop="suggested_supplier_name" label="建议供应商" min-width="120">
|
||||
<template #default="{ row }">{{ row.suggested_supplier_name || '-' }}</template>
|
||||
</t-table-column>
|
||||
<t-table-column label="交期(天)" width="80">
|
||||
<template #default="{ row }">{{ row.supplier_lead_time ?? '-' }}</template>
|
||||
</t-table-column>
|
||||
</t-table>
|
||||
|
||||
<t-empty v-if="(demandResult.items || []).length === 0" description="无物料需求(BOM 为空或订单无明细)" />
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<t-button @click="showDemandModal = false">关闭</t-button>
|
||||
</template>
|
||||
</t-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ function openCreateOrder() {
|
||||
}
|
||||
|
||||
function addMoldItem() {
|
||||
moldItems.value.push({
|
||||
moldItems.value = [...moldItems.value, {
|
||||
mold_mode: 'new',
|
||||
mold_sku: '',
|
||||
mold_name: '',
|
||||
@@ -74,11 +74,11 @@ function addMoldItem() {
|
||||
quantity: 1,
|
||||
unit_price: 0,
|
||||
remark: ''
|
||||
})
|
||||
}]
|
||||
}
|
||||
|
||||
function removeMoldItem(index: number) {
|
||||
moldItems.value.splice(index, 1)
|
||||
moldItems.value = moldItems.value.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
const orderTotalAmount = computed(() => {
|
||||
@@ -263,22 +263,22 @@ function openConsumptionModal() {
|
||||
}
|
||||
|
||||
function addConsumptionItem() {
|
||||
consumptionItems.value.push({
|
||||
consumptionItems.value = [...consumptionItems.value, {
|
||||
material_id: state.materials[0]?.id || null,
|
||||
quantity: 1,
|
||||
remark: ''
|
||||
})
|
||||
}]
|
||||
}
|
||||
|
||||
function removeConsumptionItem(index: number) {
|
||||
consumptionItems.value.splice(index, 1)
|
||||
consumptionItems.value = consumptionItems.value.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
const consumptionTotalAmount = computed(() => {
|
||||
return consumptionItems.value.reduce((sum: number, item: any) => {
|
||||
const material = state.materials.find((m: any) => m.id === item.material_id)
|
||||
const unitPrice = material?.cost_price || 0
|
||||
return sum + unitPrice * (item.quantity || 0)
|
||||
const unitPrice = Number(material?.cost_price || 0)
|
||||
return sum + unitPrice * Number(item.quantity || 0)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
@@ -604,7 +604,7 @@ onMounted(() => {
|
||||
</t-table-column>
|
||||
<t-table-column label="总价" width="120">
|
||||
<template #default="{ row: line }">
|
||||
{{ formatCurrency((state.materials.find((m: any) => m.id === line.material_id)?.cost_price || 0) * (line.quantity || 0)) }}
|
||||
{{ formatCurrency(Number(state.materials.find((m: any) => m.id === line.material_id)?.cost_price || 0) * Number(line.quantity || 0)) }}
|
||||
</template>
|
||||
</t-table-column>
|
||||
<t-table-column label="备注" width="150">
|
||||
|
||||
@@ -1,517 +1,10 @@
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { apiRequest } from '@/shared/api'
|
||||
import { addNotification, handleApiError } from '@/shared/notification'
|
||||
import { formatCurrency, formatNumber, formatDateTime, formatDate } from '@/shared/utils'
|
||||
|
||||
declare const AirDatepicker: any
|
||||
|
||||
const state = reactive({
|
||||
activeTab: 'dashboard' as string,
|
||||
backendDbReady: true,
|
||||
backendDbMessage: '' as string,
|
||||
productCategory: 'finished' as string,
|
||||
dashboard: null as any,
|
||||
financeSummary: null as any,
|
||||
financePeriod: {
|
||||
year: new Date().getFullYear(),
|
||||
quarter: '' as string
|
||||
},
|
||||
financeTransactions: [] as any[],
|
||||
receivables: [] as any[],
|
||||
payables: [] as any[],
|
||||
customerFinanceStatement: [] as any[],
|
||||
supplierFinanceStatement: [] as any[],
|
||||
customerProductStatement: [] as any[],
|
||||
supplierProductStatement: [] as any[],
|
||||
products: [] as any[],
|
||||
materials: [] as any[],
|
||||
finishedProducts: [] as any[],
|
||||
purchaseOrders: [] as any[],
|
||||
purchaseWarehouseId: null as number | null,
|
||||
purchaseReceiveItems: [] as any[],
|
||||
productionOrders: [] as any[],
|
||||
productionPlan: null as any,
|
||||
productionWarehouseId: null as number | null,
|
||||
suppliers: [] as any[],
|
||||
customers: [] as any[],
|
||||
warehouses: [] as any[],
|
||||
inventory: [] as any[],
|
||||
movements: [] as any[],
|
||||
loading: false,
|
||||
showModal: false,
|
||||
modalType: '' as string,
|
||||
editingItem: null as any,
|
||||
productBomItems: [] as any[],
|
||||
materialConsumptionItems: [] as any[],
|
||||
showMaterialConsumptionModal: false,
|
||||
consumedMaterials: [] as any[],
|
||||
restockItems: [] as any[],
|
||||
showRestockModal: false,
|
||||
form: {} as any
|
||||
})
|
||||
|
||||
let deliveryPicker: any = null
|
||||
let expectedPicker: any = null
|
||||
|
||||
const deliveryDateInput = ref<HTMLElement | null>(null)
|
||||
const expectedDateInput = ref<HTMLElement | null>(null)
|
||||
const deliveryDateNativeInput = ref<HTMLElement | null>(null)
|
||||
const expectedDateNativeInput = ref<HTMLElement | null>(null)
|
||||
/**
|
||||
* Backward-compatible wrapper — delegates to Pinia store.
|
||||
* All existing components calling `useInventory()` keep working unchanged.
|
||||
* New code should import `useInventoryStore` directly from `@/stores/inventory`.
|
||||
*/
|
||||
import { useInventoryStore } from '@/stores/inventory'
|
||||
|
||||
export function useInventory() {
|
||||
const parseDateTimeLocal = (text: string | null | undefined): Date | null => {
|
||||
if (!text) return null
|
||||
const raw = String(text).trim()
|
||||
const normalized = raw.replace('T', ' ').slice(0, 16)
|
||||
const m = normalized.match(/^(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2})$/)
|
||||
if (!m) return null
|
||||
const year = Number(m[1])
|
||||
const month = Number(m[2])
|
||||
const day = Number(m[3])
|
||||
const hour = Number(m[4])
|
||||
const minute = Number(m[5])
|
||||
if (!Number.isFinite(year + month + day + hour + minute)) return null
|
||||
return new Date(year, month - 1, day, hour, minute, 0)
|
||||
}
|
||||
|
||||
const toPickerValue = (value: any): string => {
|
||||
if (!value) return ''
|
||||
const raw = String(value).trim()
|
||||
if (/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}/.test(raw)) return raw.slice(0, 16)
|
||||
if (raw.includes('T')) return raw.replace('T', ' ').slice(0, 16)
|
||||
const dt = new Date(raw)
|
||||
if (!Number.isFinite(dt.getTime())) return ''
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
|
||||
}
|
||||
|
||||
const toApiDateTime = (value: any): string | null => {
|
||||
if (!value) return null
|
||||
const text = String(value).trim()
|
||||
if (text.includes('T')) return text.split('T')[0]
|
||||
if (text.includes(' ')) return text.split(' ')[0]
|
||||
if (text.length === 10) return text
|
||||
if (value instanceof Date) {
|
||||
const year = value.getFullYear()
|
||||
const month = String(value.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(value.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const toNativeValue = (value: any): string => {
|
||||
if (!value) return ''
|
||||
const text = String(value).trim()
|
||||
if (text.length === 10 && !text.includes('T') && !text.includes(' ')) return text
|
||||
const isoText = text.replace(' ', 'T')
|
||||
return isoText.length >= 16 ? isoText.slice(0, 16) : isoText
|
||||
}
|
||||
|
||||
const fromNativeValue = (value: any): string => {
|
||||
if (!value) return ''
|
||||
const text = String(value).trim()
|
||||
if (text.length === 10 && !text.includes('T') && !text.includes(' ')) return text
|
||||
return text.replace('T', ' ').slice(0, 16)
|
||||
}
|
||||
|
||||
const destroyPickers = () => {
|
||||
if (deliveryPicker) {
|
||||
deliveryPicker.destroy()
|
||||
deliveryPicker = null
|
||||
}
|
||||
if (expectedPicker) {
|
||||
expectedPicker.destroy()
|
||||
expectedPicker = null
|
||||
}
|
||||
}
|
||||
|
||||
const initPickers = () => {
|
||||
destroyPickers()
|
||||
if (typeof AirDatepicker !== 'function') return
|
||||
|
||||
if (state.modalType === 'salesOrder' && deliveryDateInput.value) {
|
||||
deliveryPicker = new AirDatepicker(deliveryDateInput.value, {
|
||||
timepicker: false,
|
||||
autoClose: true,
|
||||
zIndex: 2005,
|
||||
dateFormat: 'yyyy-MM-dd',
|
||||
onSelect: ({ formattedDate }: any) => {
|
||||
state.form.delivery_date = formattedDate || ''
|
||||
state.form.delivery_date_native = toNativeValue(formattedDate || '')
|
||||
}
|
||||
})
|
||||
const initial = parseDateTimeLocal(state.form.delivery_date)
|
||||
if (initial) deliveryPicker.selectDate(initial, { silent: true })
|
||||
}
|
||||
|
||||
if (state.modalType === 'purchaseOrder' && expectedDateInput.value) {
|
||||
expectedPicker = new AirDatepicker(expectedDateInput.value, {
|
||||
timepicker: false,
|
||||
autoClose: true,
|
||||
zIndex: 2005,
|
||||
dateFormat: 'yyyy-MM-dd',
|
||||
onSelect: ({ formattedDate }: any) => {
|
||||
state.form.expected_date = formattedDate || ''
|
||||
state.form.expected_date_native = toNativeValue(formattedDate || '')
|
||||
}
|
||||
})
|
||||
const initial = parseDateTimeLocal(state.form.expected_date)
|
||||
if (initial) expectedPicker.selectDate(initial, { silent: true })
|
||||
}
|
||||
}
|
||||
|
||||
const openDateTimePicker = (pickerKind: string) => {
|
||||
if (pickerKind === 'delivery' && deliveryPicker) {
|
||||
deliveryPicker.show()
|
||||
return
|
||||
}
|
||||
if (pickerKind === 'expected' && expectedPicker) {
|
||||
expectedPicker.show()
|
||||
return
|
||||
}
|
||||
|
||||
const nativeInput = pickerKind === 'delivery' ? deliveryDateNativeInput.value : expectedDateNativeInput.value
|
||||
if (!nativeInput) return
|
||||
if (typeof (nativeInput as any).showPicker === 'function') {
|
||||
(nativeInput as any).showPicker()
|
||||
return
|
||||
}
|
||||
nativeInput.focus()
|
||||
nativeInput.click()
|
||||
}
|
||||
|
||||
const getMovementTypeLabel = (movementType: string): string => {
|
||||
const movementLabelMap: Record<string, string> = {
|
||||
in: '其他入库',
|
||||
out: '其他出库',
|
||||
adjust: '库存调整',
|
||||
purchase_in: '采购入库',
|
||||
return_from_production: '生产退料入库',
|
||||
outsource_return: '外协回库',
|
||||
finish_in: '完工入库',
|
||||
issue_to_production: '生产领料出库',
|
||||
outsource_send: '外协发料出库',
|
||||
shipment_out: '销售出库',
|
||||
scrap_out: '报废出库'
|
||||
}
|
||||
return movementLabelMap[movementType] || movementType
|
||||
}
|
||||
|
||||
const getMovementBadgeClass = (movementType: string): string => {
|
||||
if (['purchase_in', 'return_from_production', 'outsource_return', 'finish_in', 'in'].includes(movementType)) {
|
||||
return 'badge-success'
|
||||
}
|
||||
if (['issue_to_production', 'outsource_send', 'shipment_out', 'scrap_out', 'out'].includes(movementType)) {
|
||||
return 'badge-error'
|
||||
}
|
||||
return 'badge-warning'
|
||||
}
|
||||
|
||||
const getPurchaseOrderStatusLabel = (status: string): string => {
|
||||
const statusMap: Record<string, string> = {
|
||||
draft: '已下单',
|
||||
pending: '已下单',
|
||||
partial_received: '部分收货',
|
||||
received: '已收货',
|
||||
paid: '已付款',
|
||||
cancelled: '已作废'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const isPurchaseOrderLocked = (status: string): boolean => {
|
||||
return ['received', 'paid', 'cancelled'].includes(status)
|
||||
}
|
||||
|
||||
const getSalesOrderStatusLabel = (status: string): string => {
|
||||
const statusMap: Record<string, string> = {
|
||||
manufacturing: '制造中',
|
||||
delivered: '已交付',
|
||||
paid: '已收款',
|
||||
cancelled: '已作废'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const getDeliveryStatusLabel = (ds: string): string => {
|
||||
const map: Record<string, string> = { manufacturing: '制造中', delivered: '已交付', cancelled: '已作废' }
|
||||
return map[ds] || ds
|
||||
}
|
||||
|
||||
const getPaymentStatusLabel = (ps: string): string => {
|
||||
const map: Record<string, string> = { unpaid: '未收款', paid: '已收款' }
|
||||
return map[ps] || ps
|
||||
}
|
||||
|
||||
const getReceiptStatusLabel = (rs: string): string => {
|
||||
const map: Record<string, string> = { pending: '已下单', partial_received: '部分收货', received: '已收货', cancelled: '已作废' }
|
||||
return map[rs] || rs
|
||||
}
|
||||
|
||||
const checkBackendHealth = async () => {
|
||||
try {
|
||||
const resp = await fetch('/health', { method: 'GET' })
|
||||
if (!resp.ok) {
|
||||
state.backendDbReady = false
|
||||
state.backendDbMessage = '后端服务异常,暂无法加载业务数据'
|
||||
return
|
||||
}
|
||||
const health = await resp.json().catch(() => null)
|
||||
if (health && health.database_connected === false) {
|
||||
state.backendDbReady = false
|
||||
state.backendDbMessage = '数据库未连接,当前仅可浏览界面,业务数据暂不可用'
|
||||
return
|
||||
}
|
||||
state.backendDbReady = true
|
||||
state.backendDbMessage = ''
|
||||
} catch {
|
||||
state.backendDbReady = false
|
||||
state.backendDbMessage = '无法连接后端服务'
|
||||
}
|
||||
}
|
||||
|
||||
const loadDashboard = async () => {
|
||||
state.loading = true
|
||||
try { state.dashboard = await apiRequest('/api/dashboard') }
|
||||
catch (e) { handleApiError(e, '加载仪表盘') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const loadFinishedProducts = async () => {
|
||||
state.loading = true
|
||||
try { state.finishedProducts = await apiRequest('/api/products?item_type=finished&limit=100') }
|
||||
catch (e) { handleApiError(e, '加载成品') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const loadProducts = async () => { await loadFinishedProducts() }
|
||||
|
||||
const loadMaterials = async () => {
|
||||
state.loading = true
|
||||
try { state.materials = await apiRequest('/api/products?item_type=material&limit=100') }
|
||||
catch (e) { handleApiError(e, '加载物料') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const loadWarehouses = async () => {
|
||||
state.loading = true
|
||||
try { state.warehouses = await apiRequest('/api/warehouses') }
|
||||
catch (e) { handleApiError(e, '加载仓库') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const ensureStockBaseData = async () => {
|
||||
if (!state.materials.length) await loadMaterials()
|
||||
if (!state.warehouses.length) await loadWarehouses()
|
||||
if (!state.warehouses.length) {
|
||||
try {
|
||||
await apiRequest('/api/warehouses', { method: 'POST', body: JSON.stringify({ name: '默认仓库' }) })
|
||||
await loadWarehouses()
|
||||
addNotification('已自动创建默认仓库', 'success')
|
||||
} catch (e) { handleApiError(e, '自动创建默认仓库') }
|
||||
}
|
||||
}
|
||||
|
||||
const loadSuppliers = async () => {
|
||||
state.loading = true
|
||||
try { state.suppliers = await apiRequest('/api/suppliers') }
|
||||
catch (e) { handleApiError(e, '加载供应商') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const loadProductionOrders = async () => {
|
||||
state.loading = true
|
||||
try {
|
||||
const [orders, warehouses] = await Promise.all([
|
||||
apiRequest('/api/sales-orders?limit=100'),
|
||||
apiRequest('/api/warehouses')
|
||||
])
|
||||
state.productionOrders = orders?.items || []
|
||||
state.warehouses = warehouses || []
|
||||
if (!state.productionWarehouseId) {
|
||||
state.productionWarehouseId = state.warehouses.find((w: any) => w.is_default)?.id || state.warehouses[0]?.id || null
|
||||
}
|
||||
} catch (e) { handleApiError(e, '加载按单生产数据') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const loadPurchaseOrders = async () => {
|
||||
state.loading = true
|
||||
try {
|
||||
const [orders, warehouses] = await Promise.all([
|
||||
apiRequest('/api/purchase-orders?limit=100'),
|
||||
apiRequest('/api/warehouses')
|
||||
])
|
||||
state.purchaseOrders = orders?.items || []
|
||||
state.warehouses = warehouses || []
|
||||
if (!state.purchaseWarehouseId) {
|
||||
state.purchaseWarehouseId = state.warehouses.find((w: any) => w.is_default)?.id || state.warehouses[0]?.id || null
|
||||
}
|
||||
} catch (e) { handleApiError(e, '加载采购订单') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const loadCustomers = async () => {
|
||||
state.loading = true
|
||||
try { state.customers = await apiRequest('/api/customers') }
|
||||
catch (e) { handleApiError(e, '加载客户') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const loadInventory = async () => {
|
||||
state.loading = true
|
||||
try { state.inventory = (await apiRequest('/api/inventory'))?.items || [] }
|
||||
catch (e) { handleApiError(e, '加载库存') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const loadMovements = async () => {
|
||||
state.loading = true
|
||||
try { state.movements = (await apiRequest('/api/stock-movements'))?.items || [] }
|
||||
catch (e) { handleApiError(e, '加载变动记录') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const loadFinance = async () => {
|
||||
state.loading = true
|
||||
try {
|
||||
const selectedYear = Number(state.financePeriod.year) || new Date().getFullYear()
|
||||
const selectedQuarter = state.financePeriod.quarter ? Number(state.financePeriod.quarter) : null
|
||||
const periodQuery = selectedQuarter
|
||||
? `year=${selectedYear}&quarter=${selectedQuarter}`
|
||||
: `year=${selectedYear}`
|
||||
const [summary, transactions, receivables, payables, customerStatement, supplierStatement, customerProductStatement, supplierProductStatement] = await Promise.all([
|
||||
apiRequest(`/api/finance/summary?${periodQuery}`),
|
||||
apiRequest(`/api/finance/transactions?status=confirmed&limit=20&${periodQuery}`),
|
||||
apiRequest(`/api/finance/receivables?limit=20&${periodQuery}`),
|
||||
apiRequest(`/api/finance/payables?limit=20&${periodQuery}`),
|
||||
apiRequest(`/api/finance/partner-statement/customer?${periodQuery}`),
|
||||
apiRequest(`/api/finance/partner-statement/supplier?${periodQuery}`),
|
||||
apiRequest(`/api/finance/partner-product-statement/customer?${periodQuery}`),
|
||||
apiRequest(`/api/finance/partner-product-statement/supplier?${periodQuery}`)
|
||||
])
|
||||
state.financeSummary = summary
|
||||
state.financeTransactions = transactions
|
||||
state.receivables = receivables
|
||||
state.payables = payables
|
||||
state.customerFinanceStatement = customerStatement.items || []
|
||||
state.supplierFinanceStatement = supplierStatement.items || []
|
||||
state.customerProductStatement = customerProductStatement.items || []
|
||||
state.supplierProductStatement = supplierProductStatement.items || []
|
||||
} catch (e) { handleApiError(e, '加载财务数据') }
|
||||
finally { state.loading = false }
|
||||
}
|
||||
|
||||
const refreshFinanceByPeriod = () => {
|
||||
if (state.activeTab === 'finance') loadFinance()
|
||||
}
|
||||
|
||||
const switchTab = (tab: string) => {
|
||||
state.activeTab = tab
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
state.showModal = false
|
||||
state.modalType = ''
|
||||
state.editingItem = null
|
||||
state.productBomItems = []
|
||||
state.purchaseReceiveItems = []
|
||||
state.form = {}
|
||||
destroyPickers()
|
||||
}
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
const prefix = state.editingItem ? '编辑' : '新增'
|
||||
const typeMap: Record<string, string> = {
|
||||
product: state.form.item_type === 'finished' ? '成品' : '物料',
|
||||
inventoryItem: '物料库存',
|
||||
salesOrder: '销售订单',
|
||||
purchaseOrder: '采购订单',
|
||||
purchaseReceive: '采购到货入库',
|
||||
supplier: '供应商',
|
||||
customer: '客户'
|
||||
}
|
||||
return prefix + (typeMap[state.modalType] || '')
|
||||
})
|
||||
|
||||
const menuGroups = [
|
||||
{ key: 'overview', title: '概览', items: [{ key: 'dashboard', label: '仪表盘' }] },
|
||||
{ key: 'sales', title: '销售', items: [{ key: 'sales_orders', label: '销售订单管理' }] },
|
||||
{ key: 'purchase', title: '采购', items: [{ key: 'purchases', label: '采购订单管理' }] },
|
||||
{ key: 'product', title: '产品', items: [{ key: 'products', label: '成品管理' }, { key: 'materials', label: '物料管理' }] },
|
||||
{ key: 'partner', title: '往来单位', items: [{ key: 'customers', label: '客户管理' }, { key: 'suppliers', label: '供应商管理' }] },
|
||||
{ key: 'warehouse', title: '仓库', items: [{ key: 'inventory', label: '库存管理' }, { key: 'movements', label: '库存变动记录' }] },
|
||||
{ key: 'finance', title: '财务', items: [{ key: 'finance', label: '财务概览' }] }
|
||||
]
|
||||
|
||||
const openGroups = reactive<Record<string, boolean>>(
|
||||
Object.fromEntries(menuGroups.map(g => [g.key, true]))
|
||||
)
|
||||
|
||||
const toggleGroup = (groupKey: string) => { openGroups[groupKey] = !openGroups[groupKey] }
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
for (const group of menuGroups) {
|
||||
const item = group.items.find(i => i.key === state.activeTab)
|
||||
if (item) return { group, item }
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const handleMenuClick = (itemKey: string) => { switchTab(itemKey) }
|
||||
|
||||
const switchProductCategory = (category: string) => { state.productCategory = category }
|
||||
|
||||
return {
|
||||
state,
|
||||
deliveryDateInput,
|
||||
expectedDateInput,
|
||||
deliveryDateNativeInput,
|
||||
expectedDateNativeInput,
|
||||
menuGroups,
|
||||
openGroups,
|
||||
activeMenu,
|
||||
modalTitle,
|
||||
parseDateTimeLocal,
|
||||
toPickerValue,
|
||||
toApiDateTime,
|
||||
toNativeValue,
|
||||
fromNativeValue,
|
||||
destroyPickers,
|
||||
initPickers,
|
||||
openDateTimePicker,
|
||||
getMovementTypeLabel,
|
||||
getMovementBadgeClass,
|
||||
getPurchaseOrderStatusLabel,
|
||||
isPurchaseOrderLocked,
|
||||
getSalesOrderStatusLabel,
|
||||
getDeliveryStatusLabel,
|
||||
getPaymentStatusLabel,
|
||||
getReceiptStatusLabel,
|
||||
checkBackendHealth,
|
||||
loadDashboard,
|
||||
loadFinishedProducts,
|
||||
loadProducts,
|
||||
loadMaterials,
|
||||
loadWarehouses,
|
||||
ensureStockBaseData,
|
||||
loadSuppliers,
|
||||
loadProductionOrders,
|
||||
loadPurchaseOrders,
|
||||
loadCustomers,
|
||||
loadInventory,
|
||||
loadMovements,
|
||||
loadFinance,
|
||||
refreshFinanceByPeriod,
|
||||
switchTab,
|
||||
closeModal,
|
||||
toggleGroup,
|
||||
handleMenuClick,
|
||||
switchProductCategory,
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
formatDateTime,
|
||||
formatDate
|
||||
}
|
||||
return useInventoryStore()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<t-button variant="text" @click="router.back()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
返回
|
||||
</t-button>
|
||||
<h1>批量分析</h1>
|
||||
<p>同时上传多个 STP 文件,并行分析、统一查看进度</p>
|
||||
</div>
|
||||
|
||||
<!-- 上传区 -->
|
||||
<div class="upload-layout" v-if="!state.batchId">
|
||||
<div class="upload-main-card">
|
||||
<h2 class="section-title">1. 选择多个 STP 文件</h2>
|
||||
<div
|
||||
:class="['upload-zone', { 'drag-over': state.dragOver }]"
|
||||
@dragover.prevent="state.dragOver = true"
|
||||
@dragleave.prevent="state.dragOver = false"
|
||||
@drop="handleDrop"
|
||||
@click="fileInput?.click()"
|
||||
>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".stp,.step"
|
||||
multiple
|
||||
@change="handleFileChange"
|
||||
hidden
|
||||
/>
|
||||
<div class="upload-icon">📁</div>
|
||||
<div class="upload-text">
|
||||
<span class="upload-title">点击选择或拖拽多个 STP/STEP 文件</span>
|
||||
<span class="upload-hint">支持批量上传,单次最多 20 个文件</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="state.selectedFiles.length" class="batch-file-list">
|
||||
<div class="batch-file-header">
|
||||
<span>已选择 {{ state.selectedFiles.length }} 个文件</span>
|
||||
<t-button variant="text" size="small" @click="state.selectedFiles = []">清空</t-button>
|
||||
</div>
|
||||
<div v-for="(file, idx) in state.selectedFiles" :key="idx" class="batch-file-item">
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<span class="file-size">{{ formatFileSize(file.size) }}</span>
|
||||
<t-button variant="text" size="small" @click="removeFile(idx)">×</t-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="state.error" class="error-message">{{ state.error }}</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-side-card">
|
||||
<h2 class="section-title">2. 注塑模参数</h2>
|
||||
<div class="material-panel compact-panel">
|
||||
<t-form-item label="产品材料">
|
||||
<t-select v-model="state.selectedMaterial">
|
||||
<t-option value="ABS" label="ABS (1.05 g/cm³)" />
|
||||
<t-option value="PP" label="PP (0.90 g/cm³)" />
|
||||
<t-option value="PE" label="PE (0.95 g/cm³)" />
|
||||
<t-option value="PC" label="PC (1.20 g/cm³)" />
|
||||
<t-option value="PA" label="PA (1.14 g/cm³)" />
|
||||
<t-option value="POM" label="POM (1.41 g/cm³)" />
|
||||
<t-option value="PMMA" label="PMMA (1.18 g/cm³)" />
|
||||
<t-option value="PBT" label="PBT (1.31 g/cm³)" />
|
||||
</t-select>
|
||||
</t-form-item>
|
||||
</div>
|
||||
|
||||
<t-button
|
||||
v-if="state.selectedFiles.length"
|
||||
theme="primary"
|
||||
class="upload-submit-btn"
|
||||
@click="batchUpload"
|
||||
:disabled="state.uploading"
|
||||
>
|
||||
{{ state.uploading ? '上传中...' : '3. 开始批量分析' }}
|
||||
</t-button>
|
||||
<div v-else class="inline-note">先选择 STP 文件,再填写材料并开始批量分析。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度看板 -->
|
||||
<div v-else class="batch-dashboard">
|
||||
<div class="batch-progress-header">
|
||||
<div class="batch-progress-info">
|
||||
<h2>批量任务:{{ state.batchId.slice(0, 8) }}...</h2>
|
||||
<p>
|
||||
共 {{ state.batchData?.total }} 个任务,
|
||||
<t-tag type="success" variant="light">完成 {{ state.batchData?.completed }}</t-tag>
|
||||
<t-tag type="danger" variant="light" v-if="state.batchData?.failed">失败 {{ state.batchData?.failed }}</t-tag>
|
||||
<t-tag type="warning" variant="light" v-if="state.batchData?.processing">进行中 {{ state.batchData?.processing }}</t-tag>
|
||||
</p>
|
||||
</div>
|
||||
<t-button
|
||||
variant="outline"
|
||||
size="small"
|
||||
@click="state.batchId = ''; state.batchData = null"
|
||||
>
|
||||
新建批量
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<!-- 整体进度条 -->
|
||||
<div class="progress-bar" style="margin-bottom: var(--space-4);">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: (state.batchData?.progress_percent || 0) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="table-container">
|
||||
<t-table :data="state.batchData?.tasks || []" row-key="task_id" stripe>
|
||||
<t-table-column title="文件名" colKey="filename" />
|
||||
<t-table-column title="状态">
|
||||
<template #default="{ row }">
|
||||
<t-tag
|
||||
:type="row.status === 'completed' ? 'success' : row.status === 'failed' ? 'danger' : 'warning'"
|
||||
variant="light"
|
||||
>
|
||||
{{ statusLabel(row.status) }}
|
||||
</t-tag>
|
||||
</template>
|
||||
</t-table-column>
|
||||
<t-table-column title="进度">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.status === 'processing'" class="progress-bar" style="width: 100px; display: inline-block;">
|
||||
<div class="progress-fill" :style="{ width: (row.progress || 0) + '%' }"></div>
|
||||
</div>
|
||||
<span v-else>{{ row.progress || 0 }}%</span>
|
||||
</template>
|
||||
</t-table-column>
|
||||
<t-table-column title="错误信息" colKey="error" />
|
||||
<t-table-column title="操作">
|
||||
<template #default="{ row }">
|
||||
<t-button
|
||||
v-if="row.status === 'completed'"
|
||||
theme="primary"
|
||||
size="small"
|
||||
@click="router.push(`/moldinsight/result/${row.task_id}`)"
|
||||
>
|
||||
查看结果
|
||||
</t-button>
|
||||
<span v-else-if="row.status === 'failed'" style="color: var(--danger-color);">
|
||||
{{ row.error || '分析失败' }}
|
||||
</span>
|
||||
<span v-else style="color: var(--text-secondary);">处理中...</span>
|
||||
</template>
|
||||
</t-table-column>
|
||||
</t-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { apiRequest } from '@/shared/api'
|
||||
import { handleApiError, addNotification } from '@/shared/notification'
|
||||
import { clearAuth } from '@/shared/auth'
|
||||
import { formatFileSize } from '@/shared/utils'
|
||||
|
||||
interface BatchTask {
|
||||
task_id: string
|
||||
filename: string
|
||||
status: string
|
||||
progress?: number
|
||||
error?: string
|
||||
html_file?: string
|
||||
}
|
||||
|
||||
interface BatchData {
|
||||
batch_id: string
|
||||
total: number
|
||||
completed: number
|
||||
failed: number
|
||||
processing: number
|
||||
progress_percent: number
|
||||
tasks: BatchTask[]
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
let pollTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const state = reactive({
|
||||
selectedFiles: [] as File[],
|
||||
selectedMaterial: 'ABS',
|
||||
dragOver: false,
|
||||
uploading: false,
|
||||
error: '',
|
||||
batchId: null as string | null,
|
||||
batchData: null as BatchData | null,
|
||||
})
|
||||
|
||||
const handleFileChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const files = Array.from(target.files || [])
|
||||
addFiles(files)
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
state.dragOver = false
|
||||
const files = Array.from(event.dataTransfer?.files || [])
|
||||
addFiles(files)
|
||||
}
|
||||
|
||||
const addFiles = (files: File[]) => {
|
||||
const valid = files.filter(f => {
|
||||
const lower = f.name.toLowerCase()
|
||||
return lower.endsWith('.stp') || lower.endsWith('.step')
|
||||
})
|
||||
if (valid.length < files.length) {
|
||||
state.error = '部分文件格式不支持,已自动过滤非 STP/STEP 文件'
|
||||
}
|
||||
const combined = [...state.selectedFiles, ...valid]
|
||||
if (combined.length > 20) {
|
||||
state.error = '单次批量上传最多 20 个文件'
|
||||
state.selectedFiles = combined.slice(0, 20)
|
||||
} else {
|
||||
state.selectedFiles = combined
|
||||
}
|
||||
}
|
||||
|
||||
const removeFile = (idx: number) => {
|
||||
state.selectedFiles.splice(idx, 1)
|
||||
}
|
||||
|
||||
const statusLabel = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
unknown: '未知',
|
||||
rejected: '已拒绝',
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const batchUpload = async () => {
|
||||
if (!appStore.token) {
|
||||
state.error = '请先登录后再上传文件'
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
if (!state.selectedFiles.length) return
|
||||
|
||||
state.uploading = true
|
||||
state.error = ''
|
||||
|
||||
const formData = new FormData()
|
||||
for (const file of state.selectedFiles) {
|
||||
formData.append('files', file)
|
||||
}
|
||||
formData.append('material', state.selectedMaterial)
|
||||
formData.append('draft_angle', '2.0')
|
||||
formData.append('shrinkage_rate', '0.5')
|
||||
formData.append('parting_precision', '0.1')
|
||||
formData.append('cavity_match', '95')
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/batch-upload', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${appStore.token}` },
|
||||
body: formData,
|
||||
})
|
||||
if (res.status === 401) {
|
||||
clearAuth()
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
if (!res.ok) throw new Error(`上传失败: ${res.status}`)
|
||||
const data = await res.json()
|
||||
state.batchId = data.batch_id
|
||||
addNotification(`已创建批量任务:${data.accepted} 个文件已接受`, 'success')
|
||||
startBatchPolling(data.batch_id)
|
||||
} catch (e) {
|
||||
state.error = handleApiError(e, '批量上传')
|
||||
} finally {
|
||||
state.uploading = false
|
||||
}
|
||||
}
|
||||
|
||||
const startBatchPolling = (batchId: string) => {
|
||||
const poll = async () => {
|
||||
try {
|
||||
const data = await apiRequest<BatchData>(`/api/batch/${batchId}`)
|
||||
state.batchData = data
|
||||
// 全部完成或全部失败则停止轮询
|
||||
if (data.processing === 0) {
|
||||
addNotification(
|
||||
`批量任务完成:${data.completed} 个成功,${data.failed} 个失败`,
|
||||
data.failed > 0 ? 'warning' : 'success'
|
||||
)
|
||||
return
|
||||
}
|
||||
pollTimer = setTimeout(poll, 3000)
|
||||
} catch (e) {
|
||||
handleApiError(e, '查询批量状态')
|
||||
}
|
||||
}
|
||||
poll()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!appStore.user) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer) clearTimeout(pollTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.batch-file-list {
|
||||
margin-top: var(--space-3);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--space-2);
|
||||
}
|
||||
.batch-file-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
.batch-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-1) 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.batch-file-item .file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.batch-file-item .file-size {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.batch-dashboard {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
.batch-progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.batch-progress-info h2 {
|
||||
margin: 0 0 var(--space-1);
|
||||
}
|
||||
.batch-progress-info p {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,9 @@
|
||||
<div class="page-header">
|
||||
<h1>注塑模 STP 分析</h1>
|
||||
<p>上传 STEP/STP 产品件,完成自动分模、工程建议与导出</p>
|
||||
<t-button variant="outline" size="small" @click="router.push('/moldinsight/batch')" style="margin-top: var(--space-2);">
|
||||
📦 批量分析
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<div class="moldinsight-intro-grid">
|
||||
|
||||
@@ -33,7 +33,17 @@
|
||||
<div class="result-card result-card-highlight" style="cursor: pointer;" @click="selectScheme(selectedScheme?.scheme_id)">
|
||||
<div class="summary-header">
|
||||
<h3>推荐方案</h3>
|
||||
<t-tag theme="primary">{{ selectedScheme?.title || selectedScheme?.scheme_id || '方案待定' }}</t-tag>
|
||||
<div class="tag-row">
|
||||
<t-tag theme="primary">{{ selectedScheme?.title || selectedScheme?.scheme_id || '方案待定' }}</t-tag>
|
||||
<t-tag
|
||||
v-if="currentAxisHint"
|
||||
theme="success"
|
||||
variant="light"
|
||||
:title="`历史 ${currentAxisHint.sample_count} 次相似产品反馈:采纳 ${currentAxisHint.adopted_count} / 拒绝 ${currentAxisHint.rejected_count}`"
|
||||
>
|
||||
📚 历史经验 {{ currentAxisHint.sample_count }} 条
|
||||
</t-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
@@ -120,12 +130,30 @@
|
||||
<t-button type="default" size="small" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
|
||||
导出 BRep
|
||||
</t-button>
|
||||
<t-button type="default" size="small" @click="createProductFromAnalysis" :loading="state.creatingProduct" title="将本次模具分析创建为进销存成品,可在进销存模块继续配置 BOM / 销售">
|
||||
📋 创建为成品
|
||||
</t-button>
|
||||
<t-button type="default" size="small" @click="estimateCost" :loading="state.costLoading" title="估算模具造价与单件成本">
|
||||
💰 成本估算
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="canGiveFeedback"
|
||||
type="default"
|
||||
size="small"
|
||||
@click="openFeedbackDialog"
|
||||
title="老师傅经验反馈:标记采纳 / 建议调整 / 拒绝,下一次同指纹产品分析将自动应用"
|
||||
>
|
||||
👍 老师傅反馈
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<div id="preview-3d" v-if="selectedHtmlFile" class="viewer-section viewer-section-hero">
|
||||
<div class="summary-header">
|
||||
<h3>3D 预览</h3>
|
||||
<t-tag theme="primary">重点区域</t-tag>
|
||||
<t-tag v-if="candidateSchemes.length > 1" theme="warning" variant="light">
|
||||
当前:{{ selectedScheme?.title || selectedScheme?.scheme_id }}
|
||||
</t-tag>
|
||||
</div>
|
||||
<t-alert v-if="state.previewStatus === 'error'" theme="warning" title="预览加载失败" :message="'HTML 已生成但加载异常,请检查该链接是否可访问:' + selectedHtmlFile" style="margin-bottom: var(--space-3);" />
|
||||
<iframe
|
||||
@@ -385,6 +413,97 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div id="cost-estimate" class="viewer-section">
|
||||
<div class="summary-header">
|
||||
<h3>成本估算</h3>
|
||||
<t-tag v-if="state.costResult" :type="state.costResult.source === 'ai' ? 'success' : 'warning'">
|
||||
{{ state.costResult.source === 'ai' ? 'AI 估算' : '规则估算' }}
|
||||
</t-tag>
|
||||
<t-tag v-else theme="primary">待生成</t-tag>
|
||||
</div>
|
||||
|
||||
<div class="result-card" style="margin-bottom: var(--space-3);">
|
||||
<t-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="state.costLoading"
|
||||
@click="estimateCost()"
|
||||
title="基于当前方案估算模具造价与单件成本"
|
||||
>
|
||||
{{ state.costLoading ? '⏳ 估算中...' : '💰 生成成本估算' }}
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<t-alert v-if="state.costError" theme="warning" title="成本估算失败" :message="state.costError" />
|
||||
|
||||
<template v-if="state.costResult">
|
||||
<div class="result-grid">
|
||||
<div class="result-card result-card-highlight">
|
||||
<h4>模具造价</h4>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="info-label">总造价</span>
|
||||
<span class="info-value" style="font-size: 1.2rem; font-weight: 700; color: var(--primary-color);">
|
||||
{{ state.costResult.total_mold_cost }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">材料费</span>
|
||||
<span class="info-value">{{ state.costResult.mold_cost.material }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">加工费</span>
|
||||
<span class="info-value">{{ state.costResult.mold_cost.machining }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">复杂度系数</span>
|
||||
<span class="info-value">{{ state.costResult.mold_cost.complexity_factor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-card">
|
||||
<h4>单件成本</h4>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="info-label">单件费用</span>
|
||||
<span class="info-value" style="font-size: 1.1rem; font-weight: 600; color: var(--warning-color);">
|
||||
{{ state.costResult.cost_per_part }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">材料用量</span>
|
||||
<span class="info-value">{{ state.costResult.part_cost.material }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">成型周期</span>
|
||||
<span class="info-value">{{ state.costResult.part_cost.cycle_time }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">置信度</span>
|
||||
<span class="info-value">{{ Math.round((state.costResult.confidence || 0) * 100) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="state.costResult.assumptions?.length" class="result-card full-width" style="margin-top: var(--space-3);">
|
||||
<h4>估算假设</h4>
|
||||
<div class="recommendations-list">
|
||||
<div
|
||||
v-for="(assumption, idx) in state.costResult.assumptions"
|
||||
:key="'cost-assumption-' + idx"
|
||||
class="recommendation-item low"
|
||||
>
|
||||
<div class="recommendation-text">ℹ️ {{ assumption }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<t-alert v-else-if="!state.costError" theme="info" title="尚未生成" message="点击“生成成本估算”后,将基于当前方案估算模具造价与单件成本。" />
|
||||
</div>
|
||||
|
||||
<div id="llm-report" class="viewer-section" v-if="hasVisibleDesignReport">
|
||||
<div class="summary-header">
|
||||
<h3>LLM 设计报告</h3>
|
||||
@@ -438,6 +557,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</t-loading>
|
||||
|
||||
<HumanFeedbackDialog
|
||||
v-model:visible="state.feedbackDialogVisible"
|
||||
:task-id="(route.params.taskId as string)"
|
||||
:scheme-id="selectedScheme?.scheme_id || ''"
|
||||
:scheme-axis="selectedScheme?.axis || 'Z'"
|
||||
:scheme-title="selectedScheme?.title || selectedScheme?.scheme_id || ''"
|
||||
:fingerprint="state.hintsFingerprint"
|
||||
:score-at-submit="selectedScheme?.score"
|
||||
:confidence-at-submit="selectedScheme?.confidence_score"
|
||||
@submitted="onFeedbackSubmitted"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -446,8 +577,10 @@ import { reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { apiRequest } from '@/shared/api'
|
||||
import { moldinsightApi } from '@/shared/api-client'
|
||||
import { handleApiError, addNotification } from '@/shared/notification'
|
||||
import { formatDateTime, formatNumber } from '@/shared/utils'
|
||||
import HumanFeedbackDialog from './components/HumanFeedbackDialog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -476,9 +609,12 @@ interface Scheme {
|
||||
summary?: string
|
||||
reason?: string
|
||||
score?: number
|
||||
score_breakdown?: { manufacturability?: number; parting_quality?: number }
|
||||
confidence_score?: number
|
||||
score_breakdown?: { manufacturability?: number; parting_quality?: number; human_hint_bonus?: number }
|
||||
mold_structure_type?: string
|
||||
offset_label?: string
|
||||
axis?: string
|
||||
method?: string
|
||||
cavity_data?: CavityData
|
||||
key_info?: KeyInfo
|
||||
html_file?: string
|
||||
@@ -561,6 +697,25 @@ interface CamOperation {
|
||||
estimated_time_min?: number
|
||||
}
|
||||
|
||||
interface CostEstimate {
|
||||
mold_cost: {
|
||||
material: string
|
||||
machining: string
|
||||
complexity_factor: string
|
||||
subtotal: string
|
||||
}
|
||||
part_cost: {
|
||||
material: string
|
||||
cycle_time: string
|
||||
cost_per_part: string
|
||||
}
|
||||
total_mold_cost: string
|
||||
cost_per_part: string
|
||||
confidence: number
|
||||
assumptions: string[]
|
||||
source?: string
|
||||
}
|
||||
|
||||
interface SideActionAiAdvice {
|
||||
source: string
|
||||
status: string
|
||||
@@ -603,7 +758,23 @@ const state = reactive({
|
||||
surface_quality: 'standard',
|
||||
controller: 'fanuc',
|
||||
include_gcode: false
|
||||
}
|
||||
},
|
||||
creatingProduct: false,
|
||||
costLoading: false,
|
||||
costError: '',
|
||||
costResult: null as CostEstimate | null,
|
||||
// D17 Human-in-Loop:老师傅经验反馈
|
||||
feedbackDialogVisible: false,
|
||||
hintsFingerprint: {} as Record<string, string>,
|
||||
hints: [] as Array<{
|
||||
scheme_axis: string
|
||||
adopted_count: number
|
||||
rejected_count: number
|
||||
adjust_count: number
|
||||
confidence: number
|
||||
weight: number
|
||||
sample_count: number
|
||||
}>,
|
||||
})
|
||||
|
||||
const camSteelOptions = [
|
||||
@@ -644,12 +815,76 @@ const loadTask = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const createProductFromAnalysis = async () => {
|
||||
const taskId = route.params.taskId as string
|
||||
try {
|
||||
state.creatingProduct = true
|
||||
const product = await apiRequest<any>(`/api/products/from-task/${taskId}`, { method: 'POST' })
|
||||
addNotification(`已创建成品:${product.name}(SKU: ${product.sku})`, 'success')
|
||||
} catch (e) {
|
||||
handleApiError(e, '创建成品')
|
||||
} finally {
|
||||
state.creatingProduct = false
|
||||
}
|
||||
}
|
||||
|
||||
// D17 Human-in-Loop:经验反馈
|
||||
const taskId = computed<string>(() => (route.params.taskId as string) || '')
|
||||
|
||||
const hintsByAxis = computed<Record<string, (typeof state.hints)[number]>>(() => {
|
||||
const map: Record<string, (typeof state.hints)[number]> = {}
|
||||
for (const h of state.hints) {
|
||||
map[h.scheme_axis] = h
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const currentAxisHint = computed<(typeof state.hints)[number] | undefined>(() => {
|
||||
const axis = selectedScheme.value?.axis
|
||||
if (!axis) return undefined
|
||||
return hintsByAxis.value[axis]
|
||||
})
|
||||
|
||||
const canGiveFeedback = computed(() => {
|
||||
const u: any = appStore.user
|
||||
if (!u) return false
|
||||
if (u.is_superuser) return true
|
||||
const roles = u.roles as Array<{ code: string }> | undefined
|
||||
return !!roles?.some(r => r.code === 'process_engineer')
|
||||
})
|
||||
|
||||
const loadExperienceHints = async () => {
|
||||
if (!taskId.value) return
|
||||
try {
|
||||
const data = await moldinsightApi.getExperienceHints(taskId.value)
|
||||
state.hints = (data.hints || []) as typeof state.hints
|
||||
state.hintsFingerprint = data.fingerprint || {}
|
||||
} catch (e) {
|
||||
// 不阻塞主流程:拉取失败时静默退化(按钮仍可点击,新反馈走 POST 写入)
|
||||
console.warn('拉取经验 hints 失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const openFeedbackDialog = () => {
|
||||
if (!selectedScheme.value?.scheme_id) {
|
||||
addNotification('当前方案未确定,无法反馈', 'error')
|
||||
return
|
||||
}
|
||||
state.feedbackDialogVisible = true
|
||||
}
|
||||
|
||||
const onFeedbackSubmitted = async () => {
|
||||
addNotification('反馈已生效,正在刷新经验角标', 'success')
|
||||
await loadExperienceHints()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!appStore.user) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
loadTask()
|
||||
loadExperienceHints()
|
||||
})
|
||||
|
||||
const getPriorityText = (priority: string) => {
|
||||
@@ -872,6 +1107,7 @@ const resultAnchorLinks = [
|
||||
{ id: 'preview-3d', label: '预览' },
|
||||
{ id: 'ai-side-action', label: 'AI倒扣分析' },
|
||||
{ id: 'export-cam', label: 'CAM/CNC' },
|
||||
{ id: 'cost-estimate', label: '成本估算' },
|
||||
{ id: 'llm-report', label: '设计报告' }
|
||||
]
|
||||
|
||||
@@ -1124,4 +1360,28 @@ const generateCamPlan = async () => {
|
||||
state.camLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
const estimateCost = async () => {
|
||||
try {
|
||||
state.costLoading = true
|
||||
state.costError = ''
|
||||
const taskId = route.params.taskId as string
|
||||
const result = await apiRequest<any>('/api/cost-estimate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ task_id: taskId })
|
||||
})
|
||||
state.costResult = result?.data || null
|
||||
addNotification(
|
||||
result?.data?.source === 'ai' ? 'AI 成本估算完成' : '规则成本估算完成',
|
||||
'success'
|
||||
)
|
||||
} catch (e: any) {
|
||||
state.costResult = null
|
||||
state.costError = e.message || '成本估算失败'
|
||||
addNotification(`成本估算失败: ${state.costError}`, 'error')
|
||||
} finally {
|
||||
state.costLoading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<t-dialog
|
||||
:visible="visible"
|
||||
@update:visible="(v: boolean) => emit('update:visible', v)"
|
||||
header="老师傅经验反馈"
|
||||
:close-on-overlay-click="true"
|
||||
width="540px"
|
||||
>
|
||||
<div v-if="schemeTitle" class="context-block">
|
||||
<div class="context-row">
|
||||
<span class="context-label">方案</span>
|
||||
<span class="context-value">{{ schemeTitle }}</span>
|
||||
</div>
|
||||
<div v-if="hasFingerprint" class="context-row">
|
||||
<span class="context-label">产品指纹</span>
|
||||
<span class="context-value">{{ formatFingerprint() }}</span>
|
||||
</div>
|
||||
<div class="context-row">
|
||||
<span class="context-label">本次应用</span>
|
||||
<span class="context-value hint-meta">写后即被下次同指纹分析消费(D17 闭环)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t-form label-width="80px">
|
||||
<t-form-item label="反馈">
|
||||
<t-radio-group v-model="form.feedback_status">
|
||||
<t-radio-button value="adopted">✅ 采纳</t-radio-button>
|
||||
<t-radio-button value="adjust">✏️ 建议调整</t-radio-button>
|
||||
<t-radio-button value="rejected">✕ 拒绝</t-radio-button>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
|
||||
<t-form-item v-if="form.feedback_status === 'adjust'" label="调整建议">
|
||||
<t-textarea
|
||||
v-model="form.adjust_suggestion"
|
||||
placeholder="具体怎么调?(如:分型面偏上 0.5mm / 增加滑块位置 / 改水路直径)"
|
||||
:maxlength="2000"
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</t-form-item>
|
||||
|
||||
<t-form-item label="原因">
|
||||
<t-textarea
|
||||
v-model="form.feedback_reason"
|
||||
placeholder="为什么这样判断?(可选,便于团队理解)"
|
||||
:maxlength="2000"
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
|
||||
<template #footer>
|
||||
<t-button theme="default" @click="cancel" :disabled="submitting">取消</t-button>
|
||||
<t-button theme="primary" :loading="submitting" @click="submit">提交反馈</t-button>
|
||||
</template>
|
||||
</t-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch, computed } from 'vue'
|
||||
import { moldinsightApi } from '@/shared/api-client'
|
||||
import { addNotification } from '@/shared/notification'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
taskId: string
|
||||
schemeId: string
|
||||
schemeAxis: string
|
||||
schemeTitle: string
|
||||
fingerprint: Record<string, string>
|
||||
scoreAtSubmit?: number
|
||||
confidenceAtSubmit?: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', v: boolean): void
|
||||
(e: 'submitted'): void
|
||||
}>()
|
||||
|
||||
interface FeedbackForm {
|
||||
feedback_status: 'adopted' | 'adjust' | 'rejected'
|
||||
feedback_reason: string
|
||||
adjust_suggestion: string
|
||||
}
|
||||
|
||||
const form = reactive<FeedbackForm>({
|
||||
feedback_status: 'adopted',
|
||||
feedback_reason: '',
|
||||
adjust_suggestion: '',
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
const hasFingerprint = computed(() => {
|
||||
return Boolean(props.fingerprint && Object.keys(props.fingerprint).length > 0)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(v) => {
|
||||
if (v) {
|
||||
// 打开时重置表单
|
||||
form.feedback_status = 'adopted'
|
||||
form.feedback_reason = ''
|
||||
form.adjust_suggestion = ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function cancel() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function formatFingerprint(): string {
|
||||
const f = props.fingerprint || {}
|
||||
const items = [
|
||||
f.bbox_aspect,
|
||||
f.volume_bucket,
|
||||
f.face_bucket,
|
||||
f.material_family,
|
||||
f.is_foam,
|
||||
].filter(Boolean)
|
||||
return items.join(' / ') || '—'
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!props.taskId || !props.schemeId) {
|
||||
addNotification('任务或方案标识缺失', 'error')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await moldinsightApi.submitExperienceFeedback(props.taskId, {
|
||||
scheme_id: props.schemeId,
|
||||
feedback_status: form.feedback_status,
|
||||
feedback_reason: form.feedback_reason || undefined,
|
||||
adjust_suggestion:
|
||||
form.feedback_status === 'adjust' ? form.adjust_suggestion || undefined : undefined,
|
||||
score_at_submit: typeof props.scoreAtSubmit === 'number' ? props.scoreAtSubmit : undefined,
|
||||
confidence_at_submit:
|
||||
typeof props.confidenceAtSubmit === 'number'
|
||||
? props.confidenceAtSubmit
|
||||
: undefined,
|
||||
})
|
||||
addNotification('反馈已提交,下次同指纹产品分析将自动应用', 'success')
|
||||
emit('submitted')
|
||||
emit('update:visible', false)
|
||||
} catch (e: any) {
|
||||
addNotification(e?.message || '反馈提交失败', 'error')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.context-block {
|
||||
background: var(--bg-secondary, #f5f7fa);
|
||||
padding: 12px 14px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--border-color, #e7e7e7);
|
||||
}
|
||||
.context-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.context-label {
|
||||
color: var(--text-secondary, #888);
|
||||
min-width: 70px;
|
||||
}
|
||||
.context-value {
|
||||
color: var(--text-primary, #333);
|
||||
font-weight: 500;
|
||||
}
|
||||
.hint-meta {
|
||||
font-weight: 400;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
@@ -240,7 +240,7 @@ const resetPassword = async (user: UserItem) => {
|
||||
try {
|
||||
await apiRequest(`/api/auth/users/${user.id}/reset-password`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(newPassword),
|
||||
body: JSON.stringify({ new_password: newPassword }),
|
||||
})
|
||||
addNotification('密码已重置', 'success')
|
||||
} catch (e) {
|
||||
|
||||
@@ -8,8 +8,25 @@ const router = createRouter({
|
||||
{ path: '/login', component: () => import('@/modules/login/LoginView.vue') },
|
||||
{ path: '/users', component: () => import('@/modules/users/UsersView.vue') },
|
||||
{ path: '/moldinsight', component: () => import('@/modules/moldinsight/MoldInsightView.vue') },
|
||||
{ path: '/moldinsight/batch', component: () => import('@/modules/moldinsight/BatchView.vue') },
|
||||
{ path: '/moldinsight/result/:taskId', component: () => import('@/modules/moldinsight/ResultView.vue') },
|
||||
{ path: '/inventory', component: () => import('@/modules/inventory/InventoryView.vue') },
|
||||
{
|
||||
path: '/inventory',
|
||||
component: () => import('@/modules/inventory/InventoryView.vue'),
|
||||
redirect: '/inventory/dashboard',
|
||||
children: [
|
||||
{ path: 'dashboard', component: () => import('@/modules/inventory/components/DashboardTab.vue') },
|
||||
{ path: 'products', component: () => import('@/modules/inventory/components/ProductsTab.vue') },
|
||||
{ path: 'materials', component: () => import('@/modules/inventory/components/MaterialsTab.vue') },
|
||||
{ path: 'inventory', component: () => import('@/modules/inventory/components/InventoryTab.vue') },
|
||||
{ path: 'purchases', component: () => import('@/modules/inventory/components/PurchaseOrdersTab.vue') },
|
||||
{ path: 'sales_orders', component: () => import('@/modules/inventory/components/SalesOrdersTab.vue') },
|
||||
{ path: 'suppliers', component: () => import('@/modules/inventory/components/SuppliersTab.vue') },
|
||||
{ path: 'customers', component: () => import('@/modules/inventory/components/CustomersTab.vue') },
|
||||
{ path: 'finance', component: () => import('@/modules/inventory/components/FinanceTab.vue') },
|
||||
{ path: 'movements', component: () => import('@/modules/inventory/components/MovementsTab.vue') },
|
||||
],
|
||||
},
|
||||
{ path: '/_design-system', component: () => import('@/design-system/DesignSystemView.vue') },
|
||||
{ path: '/_release', component: () => import('@/design-system/ReleaseView.vue') },
|
||||
],
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* Typed domain API client — wraps `apiRequest` with OpenAPI-generated types.
|
||||
*
|
||||
* Usage:
|
||||
* import { inventoryApi } from '@/shared/api-client'
|
||||
* const products = await inventoryApi.listProducts({ item_type: 'finished' })
|
||||
* // products is typed as Schema<'ProductResponse'>[]
|
||||
*/
|
||||
import { apiRequest } from './api'
|
||||
import type { Schema } from '@/types/schemas'
|
||||
|
||||
// ── Auth ───────────────────────────────────────────────────────
|
||||
export const authApi = {
|
||||
login(username: string, password: string) {
|
||||
const body = new URLSearchParams({ username, password })
|
||||
return apiRequest<Schema<'Token'>>('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
})
|
||||
},
|
||||
|
||||
loginJson(username: string, password: string) {
|
||||
return apiRequest<Schema<'Token'>>('/api/auth/login/json', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password } satisfies Schema<'LoginRequest'>),
|
||||
})
|
||||
},
|
||||
|
||||
me() {
|
||||
return apiRequest<Schema<'UserResponse'>>('/api/auth/me')
|
||||
},
|
||||
|
||||
logout() {
|
||||
return apiRequest('/api/auth/logout', { method: 'POST' })
|
||||
},
|
||||
|
||||
// Users
|
||||
listUsers() {
|
||||
return apiRequest<Schema<'UserResponse'>[]>('/api/auth/users')
|
||||
},
|
||||
|
||||
createUser(data: Schema<'UserCreate'>) {
|
||||
return apiRequest<Schema<'UserResponse'>>('/api/auth/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
updateUser(userId: number, data: Schema<'UserUpdate'>) {
|
||||
return apiRequest<Schema<'UserResponse'>>(`/api/auth/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
deleteUser(userId: number) {
|
||||
return apiRequest(`/api/auth/users/${userId}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
resetPassword(userId: number, newPassword: string) {
|
||||
return apiRequest(`/api/auth/users/${userId}/reset-password`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ new_password: newPassword }),
|
||||
})
|
||||
},
|
||||
|
||||
// Roles
|
||||
listRoles() {
|
||||
return apiRequest<Schema<'RoleResponse'>[]>('/api/auth/roles')
|
||||
},
|
||||
|
||||
createRole(data: Schema<'RoleCreate'>) {
|
||||
return apiRequest<Schema<'RoleResponse'>>('/api/auth/roles', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
updateRole(roleId: number, data: Schema<'RoleCreate'>) {
|
||||
return apiRequest<Schema<'RoleResponse'>>(`/api/auth/roles/${roleId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
deleteRole(roleId: number) {
|
||||
return apiRequest(`/api/auth/roles/${roleId}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
setRolePermissions(roleId: number, permissionIds: number[]) {
|
||||
return apiRequest(`/api/auth/roles/${roleId}/permissions`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ permission_ids: permissionIds }),
|
||||
})
|
||||
},
|
||||
|
||||
// Permissions
|
||||
listPermissions() {
|
||||
return apiRequest<Schema<'PermissionResponse'>[]>('/api/auth/permissions')
|
||||
},
|
||||
}
|
||||
|
||||
// ── Inventory / ERP ────────────────────────────────────────────
|
||||
export const inventoryApi = {
|
||||
// Dashboard
|
||||
dashboard() {
|
||||
return apiRequest('/api/dashboard')
|
||||
},
|
||||
|
||||
// Products
|
||||
listProducts(params?: { item_type?: string; limit?: number }) {
|
||||
const q = new URLSearchParams()
|
||||
if (params?.item_type) q.set('item_type', params.item_type)
|
||||
if (params?.limit) q.set('limit', String(params.limit))
|
||||
const qs = q.toString()
|
||||
return apiRequest<Schema<'ProductResponse'>[]>(`/api/products${qs ? `?${qs}` : ''}`)
|
||||
},
|
||||
|
||||
createProduct(data: Schema<'ProductCreate'>) {
|
||||
return apiRequest<Schema<'ProductResponse'>>('/api/products', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
updateProduct(id: number, data: Partial<Schema<'ProductCreate'>>) {
|
||||
return apiRequest<Schema<'ProductResponse'>>(`/api/products/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
deleteProduct(id: number) {
|
||||
return apiRequest(`/api/products/${id}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
getProductMaterials(productId: number) {
|
||||
return apiRequest<{ items: Schema<'ProductMaterialItemResponse'>[] }>(`/api/products/${productId}/materials`)
|
||||
},
|
||||
|
||||
// from-task (mold analysis → product)
|
||||
createProductFromTask(taskId: string) {
|
||||
return apiRequest<Schema<'ProductResponse'>>(`/api/products/from-task/${taskId}`, {
|
||||
method: 'POST',
|
||||
})
|
||||
},
|
||||
|
||||
// Warehouses
|
||||
listWarehouses() {
|
||||
return apiRequest<Schema<'WarehouseResponse'>[]>('/api/warehouses')
|
||||
},
|
||||
|
||||
createWarehouse(data: Schema<'WarehouseCreate'>) {
|
||||
return apiRequest<Schema<'WarehouseResponse'>>('/api/warehouses', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
// Inventory
|
||||
listInventory() {
|
||||
return apiRequest<{ items: Schema<'InventoryResponse'>[] }>('/api/inventory')
|
||||
},
|
||||
|
||||
// Stock Movements
|
||||
listStockMovements(params?: { limit?: number }) {
|
||||
const q = params?.limit ? `?limit=${params.limit}` : ''
|
||||
return apiRequest<{ items: Schema<'StockMovementResponse'>[] }>(`/api/stock-movements${q}`)
|
||||
},
|
||||
|
||||
// Suppliers
|
||||
listSuppliers() {
|
||||
return apiRequest<Schema<'SupplierResponse'>[]>('/api/suppliers')
|
||||
},
|
||||
|
||||
createSupplier(data: Schema<'SupplierCreate'>) {
|
||||
return apiRequest<Schema<'SupplierResponse'>>('/api/suppliers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
updateSupplier(id: number, data: Partial<Schema<'SupplierCreate'>>) {
|
||||
return apiRequest<Schema<'SupplierResponse'>>(`/api/suppliers/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
deleteSupplier(id: number) {
|
||||
return apiRequest(`/api/suppliers/${id}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
// Customers
|
||||
listCustomers() {
|
||||
return apiRequest<Schema<'CustomerResponse'>[]>('/api/customers')
|
||||
},
|
||||
|
||||
createCustomer(data: Schema<'CustomerCreate'>) {
|
||||
return apiRequest<Schema<'CustomerResponse'>>('/api/customers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
updateCustomer(id: number, data: Partial<Schema<'CustomerCreate'>>) {
|
||||
return apiRequest<Schema<'CustomerResponse'>>(`/api/customers/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
deleteCustomer(id: number) {
|
||||
return apiRequest(`/api/customers/${id}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
// Purchase Orders
|
||||
listPurchaseOrders(params?: { limit?: number }) {
|
||||
const q = params?.limit ? `?limit=${params.limit}` : ''
|
||||
return apiRequest<{ items: Schema<'PurchaseOrderResponse'>[] }>(`/api/purchase-orders${q}`)
|
||||
},
|
||||
|
||||
createPurchaseOrder(data: Schema<'PurchaseOrderCreate'>) {
|
||||
return apiRequest<Schema<'PurchaseOrderResponse'>>('/api/purchase-orders', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
updatePurchaseOrder(id: number, data: Partial<Schema<'PurchaseOrderCreate'>>) {
|
||||
return apiRequest<Schema<'PurchaseOrderResponse'>>(`/api/purchase-orders/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
receivePurchaseOrder(id: number, data: Schema<'PurchaseOrderReceiveRequest'>) {
|
||||
return apiRequest<Schema<'PurchaseOrderResponse'>>(`/api/purchase-orders/${id}/receive`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
updatePurchaseOrderStatus(id: number, status: string) {
|
||||
return apiRequest<Schema<'PurchaseOrderResponse'>>(`/api/purchase-orders/${id}/status`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status }),
|
||||
})
|
||||
},
|
||||
|
||||
// Sales Orders
|
||||
listSalesOrders(params?: { limit?: number }) {
|
||||
const q = params?.limit ? `?limit=${params.limit}` : ''
|
||||
return apiRequest<{ items: Schema<'SalesOrderResponse'>[] }>(`/api/sales-orders${q}`)
|
||||
},
|
||||
|
||||
getSalesOrder(id: number) {
|
||||
return apiRequest<Schema<'SalesOrderDetailResponse'>>(`/api/sales-orders/${id}`)
|
||||
},
|
||||
|
||||
createSalesOrder(data: Schema<'SalesOrderCreate'>) {
|
||||
return apiRequest<Schema<'SalesOrderResponse'>>('/api/sales-orders', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
updateSalesOrder(id: number, data: Partial<Schema<'SalesOrderCreate'>>) {
|
||||
return apiRequest<Schema<'SalesOrderResponse'>>(`/api/sales-orders/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
deleteSalesOrder(id: number) {
|
||||
return apiRequest(`/api/sales-orders/${id}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
// Purchase Demands (采购需求推导)
|
||||
calculatePurchaseDemands(salesOrderIds: number[]) {
|
||||
return apiRequest<Schema<'PurchaseDemandResponse'>>('/api/purchase-demands/calculate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ sales_order_ids: salesOrderIds } satisfies Schema<'PurchaseDemandCalculateRequest'>),
|
||||
})
|
||||
},
|
||||
|
||||
// Finance
|
||||
financeSummary(params?: { year?: number; quarter?: number }) {
|
||||
const q = new URLSearchParams()
|
||||
if (params?.year) q.set('year', String(params.year))
|
||||
if (params?.quarter) q.set('quarter', String(params.quarter))
|
||||
return apiRequest<Schema<'FinanceSummaryResponse'>>(`/api/finance/summary?${q}`)
|
||||
},
|
||||
|
||||
financeTransactions(params?: { status?: string; limit?: number; year?: number; quarter?: number }) {
|
||||
const q = new URLSearchParams()
|
||||
if (params?.status) q.set('status', params.status)
|
||||
if (params?.limit) q.set('limit', String(params.limit))
|
||||
if (params?.year) q.set('year', String(params.year))
|
||||
if (params?.quarter) q.set('quarter', String(params.quarter))
|
||||
return apiRequest<{ items: Schema<'FinanceTransactionResponse'>[] }>(`/api/finance/transactions?${q}`)
|
||||
},
|
||||
|
||||
financeReceivables(params?: { limit?: number; year?: number; quarter?: number }) {
|
||||
const q = new URLSearchParams()
|
||||
if (params?.limit) q.set('limit', String(params.limit))
|
||||
if (params?.year) q.set('year', String(params.year))
|
||||
if (params?.quarter) q.set('quarter', String(params.quarter))
|
||||
return apiRequest<Schema<'ReceivableItemResponse'>[]>(`/api/finance/receivables?${q}`)
|
||||
},
|
||||
|
||||
financePayables(params?: { limit?: number; year?: number; quarter?: number }) {
|
||||
const q = new URLSearchParams()
|
||||
if (params?.limit) q.set('limit', String(params.limit))
|
||||
if (params?.year) q.set('year', String(params.year))
|
||||
if (params?.quarter) q.set('quarter', String(params.quarter))
|
||||
return apiRequest<Schema<'PayableItemResponse'>[]>(`/api/finance/payables?${q}`)
|
||||
},
|
||||
|
||||
financePartnerStatement(partnerType: 'customer' | 'supplier', params?: { year?: number; quarter?: number }) {
|
||||
const q = new URLSearchParams()
|
||||
if (params?.year) q.set('year', String(params.year))
|
||||
if (params?.quarter) q.set('quarter', String(params.quarter))
|
||||
return apiRequest<Schema<'FinancePartnerStatementResponse'>>(`/api/finance/partner-statement/${partnerType}?${q}`)
|
||||
},
|
||||
|
||||
financePartnerProductStatement(partnerType: 'customer' | 'supplier', params?: { year?: number; quarter?: number }) {
|
||||
const q = new URLSearchParams()
|
||||
if (params?.year) q.set('year', String(params.year))
|
||||
if (params?.quarter) q.set('quarter', String(params.quarter))
|
||||
return apiRequest<Schema<'FinancePartnerProductStatementResponse'>>(`/api/finance/partner-product-statement/${partnerType}?${q}`)
|
||||
},
|
||||
}
|
||||
|
||||
// ── MoldInsight ────────────────────────────────────────────────
|
||||
export const moldinsightApi = {
|
||||
uploadStp(formData: FormData) {
|
||||
return apiRequest<{ task_id: string }>('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
},
|
||||
|
||||
getStatus(taskId: string) {
|
||||
return apiRequest<{
|
||||
task_id: string
|
||||
status: string
|
||||
progress: number
|
||||
result?: Record<string, unknown>
|
||||
error?: string
|
||||
}>(`/api/status/${taskId}`)
|
||||
},
|
||||
|
||||
batchUpload(formData: FormData) {
|
||||
return apiRequest<{
|
||||
batch_id: string
|
||||
accepted: number
|
||||
rejected: number
|
||||
task_ids: string[]
|
||||
}>('/api/batch-upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
},
|
||||
|
||||
getBatchStatus(batchId: string) {
|
||||
return apiRequest<{
|
||||
batch_id: string
|
||||
total: number
|
||||
completed: number
|
||||
failed: number
|
||||
processing: number
|
||||
progress_percent: number
|
||||
tasks: Array<{
|
||||
task_id: string
|
||||
filename: string
|
||||
status: string
|
||||
progress?: number
|
||||
error?: string
|
||||
html_file?: string
|
||||
}>
|
||||
}>(`/api/batch/${batchId}`)
|
||||
},
|
||||
|
||||
estimateCost(data: {
|
||||
task_id?: string
|
||||
material?: string
|
||||
mold_type?: string
|
||||
cavity_count?: number
|
||||
weight?: number
|
||||
dimensions?: { length: number; width: number; height: number }
|
||||
}) {
|
||||
return apiRequest<{
|
||||
mold_cost?: number
|
||||
part_cost?: number
|
||||
total_mold_cost?: number
|
||||
confidence?: number
|
||||
assumptions?: string[]
|
||||
currency?: string
|
||||
}>('/api/cost-estimate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
|
||||
// D17 Human-in-Loop:老师傅经验反馈(写入即消费闭环)
|
||||
getExperienceHints(taskId: string) {
|
||||
return apiRequest<{
|
||||
task_id: string
|
||||
stp_file_id: number
|
||||
material_name: string
|
||||
is_foam: boolean
|
||||
fingerprint: Record<string, string>
|
||||
hints: Array<{
|
||||
scheme_axis: string
|
||||
adopted_count: number
|
||||
rejected_count: number
|
||||
adjust_count: number
|
||||
confidence: number
|
||||
weight: number
|
||||
sample_count: number
|
||||
}>
|
||||
}>(`/api/tasks/${taskId}/experience-hints`)
|
||||
},
|
||||
|
||||
submitExperienceFeedback(
|
||||
taskId: string,
|
||||
data: {
|
||||
scheme_id: string
|
||||
feedback_status: 'adopted' | 'adjust' | 'rejected'
|
||||
feedback_reason?: string
|
||||
adjust_suggestion?: string
|
||||
confidence_at_submit?: number
|
||||
score_at_submit?: number
|
||||
}
|
||||
) {
|
||||
return apiRequest<{
|
||||
id: number
|
||||
scheme_id: string
|
||||
scheme_axis: string
|
||||
feedback_status: string
|
||||
created_at: string
|
||||
}>(`/api/tasks/${taskId}/experience-feedback`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { apiRequest } from '@/shared/api'
|
||||
import { addNotification, handleApiError } from '@/shared/notification'
|
||||
import { formatCurrency, formatNumber, formatDateTime, formatDate } from '@/shared/utils'
|
||||
import type { Schema } from '@/types/schemas'
|
||||
|
||||
declare const AirDatepicker: any
|
||||
|
||||
// Dashboard has no Pydantic schema in backend, define it here
|
||||
interface DashboardData {
|
||||
finished_product_count: number
|
||||
total_stock: number
|
||||
total_value: number
|
||||
supplier_count: number
|
||||
customer_count: number
|
||||
warehouse_count: number
|
||||
}
|
||||
|
||||
export const useInventoryStore = defineStore('inventory', () => {
|
||||
// ── state ──
|
||||
const activeTab = ref('dashboard')
|
||||
const backendDbReady = ref(true)
|
||||
const backendDbMessage = ref('')
|
||||
const productCategory = ref('finished')
|
||||
const dashboard = ref<DashboardData | null>(null)
|
||||
const financeSummary = ref<Schema<'FinanceSummaryResponse'> | null>(null)
|
||||
const financePeriod = reactive({ year: new Date().getFullYear(), quarter: '' as string })
|
||||
const financeTransactions = ref<Schema<'FinanceTransactionResponse'>[]>([])
|
||||
const receivables = ref<Schema<'ReceivableItemResponse'>[]>([])
|
||||
const payables = ref<Schema<'PayableItemResponse'>[]>([])
|
||||
const customerFinanceStatement = ref<Schema<'PartnerStatementItemResponse'>[]>([])
|
||||
const supplierFinanceStatement = ref<Schema<'PartnerStatementItemResponse'>[]>([])
|
||||
const customerProductStatement = ref<Schema<'PartnerProductStatementItemResponse'>[]>([])
|
||||
const supplierProductStatement = ref<Schema<'PartnerProductStatementItemResponse'>[]>([])
|
||||
const products = ref<Schema<'ProductResponse'>[]>([])
|
||||
const materials = ref<Schema<'ProductResponse'>[]>([])
|
||||
const finishedProducts = ref<Schema<'ProductResponse'>[]>([])
|
||||
const purchaseOrders = ref<Schema<'PurchaseOrderResponse'>[]>([])
|
||||
const purchaseWarehouseId = ref<number | null>(null)
|
||||
const purchaseReceiveItems = ref<any[]>([])
|
||||
const productionOrders = ref<Schema<'SalesOrderResponse'>[]>([])
|
||||
const productionPlan = ref<any>(null)
|
||||
const productionWarehouseId = ref<number | null>(null)
|
||||
const suppliers = ref<Schema<'SupplierResponse'>[]>([])
|
||||
const customers = ref<Schema<'CustomerResponse'>[]>([])
|
||||
const warehouses = ref<Schema<'WarehouseResponse'>[]>([])
|
||||
const inventory = ref<Schema<'InventoryResponse'>[]>([])
|
||||
const movements = ref<Schema<'StockMovementResponse'>[]>([])
|
||||
const loading = ref(false)
|
||||
const showModal = ref(false)
|
||||
const modalType = ref('')
|
||||
const editingItem = ref<any>(null)
|
||||
const productBomItems = ref<any[]>([])
|
||||
const materialConsumptionItems = ref<any[]>([])
|
||||
const showMaterialConsumptionModal = ref(false)
|
||||
const consumedMaterials = ref<any[]>([])
|
||||
const restockItems = ref<any[]>([])
|
||||
const showRestockModal = ref(false)
|
||||
const form = ref<any>({})
|
||||
|
||||
// ── date-picker refs ──
|
||||
let deliveryPicker: any = null
|
||||
let expectedPicker: any = null
|
||||
const deliveryDateInput = ref<HTMLElement | null>(null)
|
||||
const expectedDateInput = ref<HTMLElement | null>(null)
|
||||
const deliveryDateNativeInput = ref<HTMLElement | null>(null)
|
||||
const expectedDateNativeInput = ref<HTMLElement | null>(null)
|
||||
|
||||
// ── legacy state object (backward-compat for components still using `state.xxx`) ──
|
||||
const state = reactive({
|
||||
get activeTab() { return activeTab.value }, set activeTab(v) { activeTab.value = v },
|
||||
get backendDbReady() { return backendDbReady.value }, set backendDbReady(v) { backendDbReady.value = v },
|
||||
get backendDbMessage() { return backendDbMessage.value }, set backendDbMessage(v) { backendDbMessage.value = v },
|
||||
get productCategory() { return productCategory.value }, set productCategory(v) { productCategory.value = v },
|
||||
get dashboard() { return dashboard.value }, set dashboard(v) { dashboard.value = v },
|
||||
get financeSummary() { return financeSummary.value }, set financeSummary(v) { financeSummary.value = v },
|
||||
financePeriod,
|
||||
get financeTransactions() { return financeTransactions.value }, set financeTransactions(v) { financeTransactions.value = v },
|
||||
get receivables() { return receivables.value }, set receivables(v) { receivables.value = v },
|
||||
get payables() { return payables.value }, set payables(v) { payables.value = v },
|
||||
get customerFinanceStatement() { return customerFinanceStatement.value }, set customerFinanceStatement(v) { customerFinanceStatement.value = v },
|
||||
get supplierFinanceStatement() { return supplierFinanceStatement.value }, set supplierFinanceStatement(v) { supplierFinanceStatement.value = v },
|
||||
get customerProductStatement() { return customerProductStatement.value }, set customerProductStatement(v) { customerProductStatement.value = v },
|
||||
get supplierProductStatement() { return supplierProductStatement.value }, set supplierProductStatement(v) { supplierProductStatement.value = v },
|
||||
get products() { return products.value }, set products(v) { products.value = v },
|
||||
get materials() { return materials.value }, set materials(v) { materials.value = v },
|
||||
get finishedProducts() { return finishedProducts.value }, set finishedProducts(v) { finishedProducts.value = v },
|
||||
get purchaseOrders() { return purchaseOrders.value }, set purchaseOrders(v) { purchaseOrders.value = v },
|
||||
get purchaseWarehouseId() { return purchaseWarehouseId.value }, set purchaseWarehouseId(v) { purchaseWarehouseId.value = v },
|
||||
get purchaseReceiveItems() { return purchaseReceiveItems.value }, set purchaseReceiveItems(v) { purchaseReceiveItems.value = v },
|
||||
get productionOrders() { return productionOrders.value }, set productionOrders(v) { productionOrders.value = v },
|
||||
get productionPlan() { return productionPlan.value }, set productionPlan(v) { productionPlan.value = v },
|
||||
get productionWarehouseId() { return productionWarehouseId.value }, set productionWarehouseId(v) { productionWarehouseId.value = v },
|
||||
get suppliers() { return suppliers.value }, set suppliers(v) { suppliers.value = v },
|
||||
get customers() { return customers.value }, set customers(v) { customers.value = v },
|
||||
get warehouses() { return warehouses.value }, set warehouses(v) { warehouses.value = v },
|
||||
get inventory() { return inventory.value }, set inventory(v) { inventory.value = v },
|
||||
get movements() { return movements.value }, set movements(v) { movements.value = v },
|
||||
get loading() { return loading.value }, set loading(v) { loading.value = v },
|
||||
get showModal() { return showModal.value }, set showModal(v) { showModal.value = v },
|
||||
get modalType() { return modalType.value }, set modalType(v) { modalType.value = v },
|
||||
get editingItem() { return editingItem.value }, set editingItem(v) { editingItem.value = v },
|
||||
get productBomItems() { return productBomItems.value }, set productBomItems(v) { productBomItems.value = v },
|
||||
get materialConsumptionItems() { return materialConsumptionItems.value }, set materialConsumptionItems(v) { materialConsumptionItems.value = v },
|
||||
get showMaterialConsumptionModal() { return showMaterialConsumptionModal.value }, set showMaterialConsumptionModal(v) { showMaterialConsumptionModal.value = v },
|
||||
get consumedMaterials() { return consumedMaterials.value }, set consumedMaterials(v) { consumedMaterials.value = v },
|
||||
get restockItems() { return restockItems.value }, set restockItems(v) { restockItems.value = v },
|
||||
get showRestockModal() { return showRestockModal.value }, set showRestockModal(v) { showRestockModal.value = v },
|
||||
get form() { return form.value }, set form(v) { form.value = v },
|
||||
})
|
||||
|
||||
// ── helpers ──
|
||||
const parseDateTimeLocal = (text: string | null | undefined): Date | null => {
|
||||
if (!text) return null
|
||||
const raw = String(text).trim()
|
||||
const normalized = raw.replace('T', ' ').slice(0, 16)
|
||||
const m = normalized.match(/^(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2})$/)
|
||||
if (!m) return null
|
||||
const year = Number(m[1]); const month = Number(m[2]); const day = Number(m[3])
|
||||
const hour = Number(m[4]); const minute = Number(m[5])
|
||||
if (!Number.isFinite(year + month + day + hour + minute)) return null
|
||||
return new Date(year, month - 1, day, hour, minute, 0)
|
||||
}
|
||||
|
||||
const toPickerValue = (value: any): string => {
|
||||
if (!value) return ''
|
||||
const raw = String(value).trim()
|
||||
if (/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}/.test(raw)) return raw.slice(0, 16)
|
||||
if (raw.includes('T')) return raw.replace('T', ' ').slice(0, 16)
|
||||
const dt = new Date(raw)
|
||||
if (!Number.isFinite(dt.getTime())) return ''
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
|
||||
}
|
||||
|
||||
const toApiDateTime = (value: any): string | null => {
|
||||
if (!value) return null
|
||||
const text = String(value).trim()
|
||||
if (text.includes('T')) return text.split('T')[0]
|
||||
if (text.includes(' ')) return text.split(' ')[0]
|
||||
if (text.length === 10) return text
|
||||
if (value instanceof Date) {
|
||||
const year = value.getFullYear()
|
||||
const month = String(value.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(value.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const toNativeValue = (value: any): string => {
|
||||
if (!value) return ''
|
||||
const text = String(value).trim()
|
||||
if (text.length === 10 && !text.includes('T') && !text.includes(' ')) return text
|
||||
const isoText = text.replace(' ', 'T')
|
||||
return isoText.length >= 16 ? isoText.slice(0, 16) : isoText
|
||||
}
|
||||
|
||||
const fromNativeValue = (value: any): string => {
|
||||
if (!value) return ''
|
||||
const text = String(value).trim()
|
||||
if (text.length === 10 && !text.includes('T') && !text.includes(' ')) return text
|
||||
return text.replace('T', ' ').slice(0, 16)
|
||||
}
|
||||
|
||||
// ── pickers ──
|
||||
const destroyPickers = () => {
|
||||
if (deliveryPicker) { deliveryPicker.destroy(); deliveryPicker = null }
|
||||
if (expectedPicker) { expectedPicker.destroy(); expectedPicker = null }
|
||||
}
|
||||
|
||||
const initPickers = () => {
|
||||
destroyPickers()
|
||||
if (typeof AirDatepicker !== 'function') return
|
||||
if (modalType.value === 'salesOrder' && deliveryDateInput.value) {
|
||||
deliveryPicker = new AirDatepicker(deliveryDateInput.value, {
|
||||
timepicker: false, autoClose: true, zIndex: 2005, dateFormat: 'yyyy-MM-dd',
|
||||
onSelect: ({ formattedDate }: any) => {
|
||||
form.value.delivery_date = formattedDate || ''
|
||||
form.value.delivery_date_native = toNativeValue(formattedDate || '')
|
||||
}
|
||||
})
|
||||
const initial = parseDateTimeLocal(form.value.delivery_date)
|
||||
if (initial) deliveryPicker.selectDate(initial, { silent: true })
|
||||
}
|
||||
if (modalType.value === 'purchaseOrder' && expectedDateInput.value) {
|
||||
expectedPicker = new AirDatepicker(expectedDateInput.value, {
|
||||
timepicker: false, autoClose: true, zIndex: 2005, dateFormat: 'yyyy-MM-dd',
|
||||
onSelect: ({ formattedDate }: any) => {
|
||||
form.value.expected_date = formattedDate || ''
|
||||
form.value.expected_date_native = toNativeValue(formattedDate || '')
|
||||
}
|
||||
})
|
||||
const initial = parseDateTimeLocal(form.value.expected_date)
|
||||
if (initial) expectedPicker.selectDate(initial, { silent: true })
|
||||
}
|
||||
}
|
||||
|
||||
const openDateTimePicker = (pickerKind: string) => {
|
||||
if (pickerKind === 'delivery' && deliveryPicker) { deliveryPicker.show(); return }
|
||||
if (pickerKind === 'expected' && expectedPicker) { expectedPicker.show(); return }
|
||||
const nativeInput = pickerKind === 'delivery' ? deliveryDateNativeInput.value : expectedDateNativeInput.value
|
||||
if (!nativeInput) return
|
||||
if (typeof (nativeInput as any).showPicker === 'function') { (nativeInput as any).showPicker(); return }
|
||||
nativeInput.focus(); nativeInput.click()
|
||||
}
|
||||
|
||||
// ── label helpers ──
|
||||
const getMovementTypeLabel = (movementType: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
in: '其他入库', out: '其他出库', adjust: '库存调整',
|
||||
purchase_in: '采购入库', return_from_production: '生产退料入库',
|
||||
outsource_return: '外协回库', finish_in: '完工入库',
|
||||
issue_to_production: '生产领料出库', outsource_send: '外协发料出库',
|
||||
shipment_out: '销售出库', scrap_out: '报废出库'
|
||||
}
|
||||
return map[movementType] || movementType
|
||||
}
|
||||
|
||||
const getMovementBadgeClass = (movementType: string): string => {
|
||||
if (['purchase_in', 'return_from_production', 'outsource_return', 'finish_in', 'in'].includes(movementType)) return 'badge-success'
|
||||
if (['issue_to_production', 'outsource_send', 'shipment_out', 'scrap_out', 'out'].includes(movementType)) return 'badge-error'
|
||||
return 'badge-warning'
|
||||
}
|
||||
|
||||
const getPurchaseOrderStatusLabel = (status: string): string => {
|
||||
const map: Record<string, string> = { draft: '已下单', pending: '已下单', partial_received: '部分收货', received: '已收货', paid: '已付款', cancelled: '已作废' }
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const isPurchaseOrderLocked = (status: string): boolean => ['received', 'paid', 'cancelled'].includes(status)
|
||||
|
||||
const getSalesOrderStatusLabel = (status: string): string => {
|
||||
const map: Record<string, string> = { manufacturing: '制造中', delivered: '已交付', paid: '已收款', cancelled: '已作废' }
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const getDeliveryStatusLabel = (ds: string): string => {
|
||||
const map: Record<string, string> = { manufacturing: '制造中', delivered: '已交付', cancelled: '已作废' }
|
||||
return map[ds] || ds
|
||||
}
|
||||
|
||||
const getPaymentStatusLabel = (ps: string): string => {
|
||||
const map: Record<string, string> = { unpaid: '未收款', paid: '已收款' }
|
||||
return map[ps] || ps
|
||||
}
|
||||
|
||||
const getReceiptStatusLabel = (rs: string): string => {
|
||||
const map: Record<string, string> = { pending: '已下单', partial_received: '部分收货', received: '已收货', cancelled: '已作废' }
|
||||
return map[rs] || rs
|
||||
}
|
||||
|
||||
// ── data loading ──
|
||||
const checkBackendHealth = async () => {
|
||||
try {
|
||||
const resp = await fetch('/health', { method: 'GET' })
|
||||
if (!resp.ok) { backendDbReady.value = false; backendDbMessage.value = '后端服务异常,暂无法加载业务数据'; return }
|
||||
const health = await resp.json().catch(() => null)
|
||||
if (health && health.database_connected === false) { backendDbReady.value = false; backendDbMessage.value = '数据库未连接,当前仅可浏览界面,业务数据暂不可用'; return }
|
||||
backendDbReady.value = true; backendDbMessage.value = ''
|
||||
} catch { backendDbReady.value = false; backendDbMessage.value = '无法连接后端服务' }
|
||||
}
|
||||
|
||||
const loadDashboard = async () => {
|
||||
loading.value = true
|
||||
try { dashboard.value = await apiRequest('/api/dashboard') }
|
||||
catch (e) { handleApiError(e, '加载仪表盘') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const loadFinishedProducts = async () => {
|
||||
loading.value = true
|
||||
try { finishedProducts.value = await apiRequest('/api/products?item_type=finished&limit=100') }
|
||||
catch (e) { handleApiError(e, '加载成品') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const loadProducts = async () => { await loadFinishedProducts() }
|
||||
|
||||
const loadMaterials = async () => {
|
||||
loading.value = true
|
||||
try { materials.value = await apiRequest('/api/products?item_type=material&limit=100') }
|
||||
catch (e) { handleApiError(e, '加载物料') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const loadWarehouses = async () => {
|
||||
loading.value = true
|
||||
try { warehouses.value = await apiRequest('/api/warehouses') }
|
||||
catch (e) { handleApiError(e, '加载仓库') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const ensureStockBaseData = async () => {
|
||||
if (!materials.value.length) await loadMaterials()
|
||||
if (!warehouses.value.length) await loadWarehouses()
|
||||
if (!warehouses.value.length) {
|
||||
try {
|
||||
await apiRequest('/api/warehouses', { method: 'POST', body: JSON.stringify({ name: '默认仓库' }) })
|
||||
await loadWarehouses()
|
||||
addNotification('已自动创建默认仓库', 'success')
|
||||
} catch (e) { handleApiError(e, '自动创建默认仓库') }
|
||||
}
|
||||
}
|
||||
|
||||
const loadSuppliers = async () => {
|
||||
loading.value = true
|
||||
try { suppliers.value = await apiRequest('/api/suppliers') }
|
||||
catch (e) { handleApiError(e, '加载供应商') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const loadProductionOrders = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [orders, wh] = await Promise.all([apiRequest('/api/sales-orders?limit=100'), apiRequest('/api/warehouses')])
|
||||
productionOrders.value = orders?.items || []
|
||||
warehouses.value = wh || []
|
||||
if (!productionWarehouseId.value) {
|
||||
productionWarehouseId.value = warehouses.value.find((w: any) => w.is_default)?.id || warehouses.value[0]?.id || null
|
||||
}
|
||||
} catch (e) { handleApiError(e, '加载按单生产数据') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const loadPurchaseOrders = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [orders, wh] = await Promise.all([apiRequest('/api/purchase-orders?limit=100'), apiRequest('/api/warehouses')])
|
||||
purchaseOrders.value = orders?.items || []
|
||||
warehouses.value = wh || []
|
||||
if (!purchaseWarehouseId.value) {
|
||||
purchaseWarehouseId.value = warehouses.value.find((w: any) => w.is_default)?.id || warehouses.value[0]?.id || null
|
||||
}
|
||||
} catch (e) { handleApiError(e, '加载采购订单') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const loadCustomers = async () => {
|
||||
loading.value = true
|
||||
try { customers.value = await apiRequest('/api/customers') }
|
||||
catch (e) { handleApiError(e, '加载客户') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const loadInventory = async () => {
|
||||
loading.value = true
|
||||
try { inventory.value = (await apiRequest('/api/inventory'))?.items || [] }
|
||||
catch (e) { handleApiError(e, '加载库存') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const loadMovements = async () => {
|
||||
loading.value = true
|
||||
try { movements.value = (await apiRequest('/api/stock-movements'))?.items || [] }
|
||||
catch (e) { handleApiError(e, '加载变动记录') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const loadFinance = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const selectedYear = Number(financePeriod.year) || new Date().getFullYear()
|
||||
const selectedQuarter = financePeriod.quarter ? Number(financePeriod.quarter) : null
|
||||
const periodQuery = selectedQuarter ? `year=${selectedYear}&quarter=${selectedQuarter}` : `year=${selectedYear}`
|
||||
const [summary, transactions, recv, pay, custStmt, suppStmt, custProdStmt, suppProdStmt] = await Promise.all([
|
||||
apiRequest(`/api/finance/summary?${periodQuery}`),
|
||||
apiRequest(`/api/finance/transactions?status=confirmed&limit=20&${periodQuery}`),
|
||||
apiRequest(`/api/finance/receivables?limit=20&${periodQuery}`),
|
||||
apiRequest(`/api/finance/payables?limit=20&${periodQuery}`),
|
||||
apiRequest(`/api/finance/partner-statement/customer?${periodQuery}`),
|
||||
apiRequest(`/api/finance/partner-statement/supplier?${periodQuery}`),
|
||||
apiRequest(`/api/finance/partner-product-statement/customer?${periodQuery}`),
|
||||
apiRequest(`/api/finance/partner-product-statement/supplier?${periodQuery}`)
|
||||
])
|
||||
financeSummary.value = summary
|
||||
financeTransactions.value = transactions?.items || []
|
||||
receivables.value = recv
|
||||
payables.value = pay
|
||||
customerFinanceStatement.value = custStmt.items || []
|
||||
supplierFinanceStatement.value = suppStmt.items || []
|
||||
customerProductStatement.value = custProdStmt.items || []
|
||||
supplierProductStatement.value = suppProdStmt.items || []
|
||||
} catch (e) { handleApiError(e, '加载财务数据') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const refreshFinanceByPeriod = () => { if (activeTab.value === 'finance') loadFinance() }
|
||||
|
||||
const switchTab = (tab: string) => { activeTab.value = tab }
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false; modalType.value = ''; editingItem.value = null
|
||||
productBomItems.value = []; purchaseReceiveItems.value = []; form.value = {}
|
||||
destroyPickers()
|
||||
}
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
const prefix = editingItem.value ? '编辑' : '新增'
|
||||
const typeMap: Record<string, string> = {
|
||||
product: form.value.item_type === 'finished' ? '成品' : '物料',
|
||||
inventoryItem: '物料库存', salesOrder: '销售订单', purchaseOrder: '采购订单',
|
||||
purchaseReceive: '采购到货入库', supplier: '供应商', customer: '客户'
|
||||
}
|
||||
return prefix + (typeMap[modalType.value] || '')
|
||||
})
|
||||
|
||||
const menuGroups = [
|
||||
{ key: 'overview', title: '概览', items: [{ key: 'dashboard', label: '仪表盘' }] },
|
||||
{ key: 'sales', title: '销售', items: [{ key: 'sales_orders', label: '销售订单管理' }] },
|
||||
{ key: 'purchase', title: '采购', items: [{ key: 'purchases', label: '采购订单管理' }] },
|
||||
{ key: 'product', title: '产品', items: [{ key: 'products', label: '成品管理' }, { key: 'materials', label: '物料管理' }] },
|
||||
{ key: 'partner', title: '往来单位', items: [{ key: 'customers', label: '客户管理' }, { key: 'suppliers', label: '供应商管理' }] },
|
||||
{ key: 'warehouse', title: '仓库', items: [{ key: 'inventory', label: '库存管理' }, { key: 'movements', label: '库存变动记录' }] },
|
||||
{ key: 'finance', title: '财务', items: [{ key: 'finance', label: '财务概览' }] }
|
||||
]
|
||||
|
||||
const openGroups = reactive<Record<string, boolean>>(
|
||||
Object.fromEntries(menuGroups.map(g => [g.key, true]))
|
||||
)
|
||||
|
||||
const toggleGroup = (groupKey: string) => { openGroups[groupKey] = !openGroups[groupKey] }
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
for (const group of menuGroups) {
|
||||
const item = group.items.find(i => i.key === activeTab.value)
|
||||
if (item) return { group, item }
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const handleMenuClick = (itemKey: string) => { switchTab(itemKey) }
|
||||
const switchProductCategory = (category: string) => { productCategory.value = category }
|
||||
|
||||
return {
|
||||
// state refs (new Pinia style)
|
||||
activeTab, backendDbReady, backendDbMessage, productCategory, dashboard,
|
||||
financeSummary, financePeriod, financeTransactions, receivables, payables,
|
||||
customerFinanceStatement, supplierFinanceStatement, customerProductStatement, supplierProductStatement,
|
||||
products, materials, finishedProducts, purchaseOrders, purchaseWarehouseId, purchaseReceiveItems,
|
||||
productionOrders, productionPlan, productionWarehouseId, suppliers, customers, warehouses,
|
||||
inventory, movements, loading, showModal, modalType, editingItem, productBomItems,
|
||||
materialConsumptionItems, showMaterialConsumptionModal, consumedMaterials,
|
||||
restockItems, showRestockModal, form,
|
||||
// legacy state object (backward compat)
|
||||
state,
|
||||
// refs
|
||||
deliveryDateInput, expectedDateInput, deliveryDateNativeInput, expectedDateNativeInput,
|
||||
// computed
|
||||
menuGroups, openGroups, activeMenu, modalTitle,
|
||||
// actions
|
||||
parseDateTimeLocal, toPickerValue, toApiDateTime, toNativeValue, fromNativeValue,
|
||||
destroyPickers, initPickers, openDateTimePicker,
|
||||
getMovementTypeLabel, getMovementBadgeClass, getPurchaseOrderStatusLabel, isPurchaseOrderLocked,
|
||||
getSalesOrderStatusLabel, getDeliveryStatusLabel, getPaymentStatusLabel, getReceiptStatusLabel,
|
||||
checkBackendHealth, loadDashboard, loadFinishedProducts, loadProducts, loadMaterials,
|
||||
loadWarehouses, ensureStockBaseData, loadSuppliers, loadProductionOrders, loadPurchaseOrders,
|
||||
loadCustomers, loadInventory, loadMovements, loadFinance, refreshFinanceByPeriod,
|
||||
switchTab, closeModal, toggleGroup, handleMenuClick, switchProductCategory,
|
||||
formatCurrency, formatNumber, formatDateTime, formatDate
|
||||
}
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 后端 OpenAPI schema 类型快捷引用
|
||||
*
|
||||
* 类型由 `npm run gen:api` 从后端 openapi-inventory.json 自动生成(见 ./api.ts)。
|
||||
* 后端字段变更后重新生成即可,前端类型自动同步,杜绝手写类型漂移。
|
||||
*
|
||||
* 用法:Schema<'FinanceSummaryResponse'>、Schema<'SalesOrderResponse'>[]
|
||||
*/
|
||||
import type { components } from './api'
|
||||
|
||||
export type Schema<K extends keyof components['schemas']> = components['schemas'][K]
|
||||
+11
-3
@@ -2,16 +2,16 @@ import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
export default defineConfig(() => ({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
base: mode === 'development' ? '/' : '/static/',
|
||||
base: '/',
|
||||
build: {
|
||||
outDir: '../static',
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
@@ -21,6 +21,14 @@ export default defineConfig(({ mode }) => ({
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/health': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/html': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Alembic 迁移环境配置
|
||||
|
||||
- 从 shared.config.settings 读取 DB 配置,构造同步 URL(psycopg2)供 alembic 使用
|
||||
(项目运行时用 asyncpg,但 alembic 是同步库,需 psycopg2)
|
||||
- target_metadata 指向 shared.models.base.Base.metadata(全量模型注册见下方 import)
|
||||
- 支持 ALEMBIC_URL 环境变量覆盖(用于离线/空库生成初始迁移,如 sqlite:///empty.db)
|
||||
"""
|
||||
from logging.config import fileConfig
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from alembic import context
|
||||
|
||||
# 让 alembic 能 import 项目模块(src 在项目根下)
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root / "src"))
|
||||
|
||||
from shared.config.settings import settings # noqa: E402
|
||||
from shared.models.base import Base # noqa: E402
|
||||
# 导入全部三包模型,确保 metadata 注册(全量注册点约定见 shared/models/base.py)
|
||||
import shared.models.identity # noqa: E402,F401
|
||||
import moldinsight.models # noqa: E402,F401
|
||||
import inventory.models # noqa: E402,F401
|
||||
|
||||
config = context.config
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# 构造同步 URL:asyncpg -> psycopg2
|
||||
_sync_url = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql+psycopg2://")
|
||||
# 支持 ALEMBIC_URL 覆盖(离线生成/测试用)
|
||||
config.set_main_option("sqlalchemy.url", os.getenv("ALEMBIC_URL", _sync_url))
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""离线模式:生成 SQL 脚本,不连接 DB"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""在线模式:连接 DB 执行迁移"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,38 @@
|
||||
"""add product_id to stp_files
|
||||
|
||||
Revision ID: 006c18c51b0d
|
||||
Revises: 9928d7f8c1ef
|
||||
Create Date: 2026-07-23 10:37:56.516787
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '006c18c51b0d'
|
||||
down_revision: Union[str, Sequence[str], None] = '9928d7f8c1ef'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema: stp_files 加 product_id 外键,关联进销存成品。"""
|
||||
op.add_column("stp_files", sa.Column("product_id", sa.Integer(), nullable=True))
|
||||
op.create_index("ix_stp_files_product_id", "stp_files", ["product_id"])
|
||||
op.create_foreign_key(
|
||||
"fk_stp_files_product_id_products",
|
||||
"stp_files",
|
||||
"products",
|
||||
["product_id"],
|
||||
["id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_constraint("fk_stp_files_product_id_products", "stp_files", type_="foreignkey")
|
||||
op.drop_index("ix_stp_files_product_id", table_name="stp_files")
|
||||
op.drop_column("stp_files", "product_id")
|
||||
@@ -0,0 +1,728 @@
|
||||
"""initial schema
|
||||
|
||||
Revision ID: 9928d7f8c1ef
|
||||
Revises:
|
||||
Create Date: 2026-07-20 10:30:30.249866
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '9928d7f8c1ef'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('customers',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=50), nullable=True),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('contact_person', sa.String(length=100), nullable=True),
|
||||
sa.Column('phone', sa.String(length=50), nullable=True),
|
||||
sa.Column('email', sa.String(length=100), nullable=True),
|
||||
sa.Column('address', sa.Text(), nullable=True),
|
||||
sa.Column('bank_name', sa.String(length=100), nullable=True),
|
||||
sa.Column('bank_account', sa.String(length=50), nullable=True),
|
||||
sa.Column('tax_number', sa.String(length=50), nullable=True),
|
||||
sa.Column('credit_limit', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_customers_code'), 'customers', ['code'], unique=True)
|
||||
op.create_index(op.f('ix_customers_id'), 'customers', ['id'], unique=False)
|
||||
op.create_table('permissions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=100), nullable=False),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.Column('module', sa.String(length=50), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_permissions_code'), 'permissions', ['code'], unique=True)
|
||||
op.create_index(op.f('ix_permissions_id'), 'permissions', ['id'], unique=False)
|
||||
op.create_table('products',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('sku', sa.String(length=50), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('category', sa.String(length=100), nullable=True),
|
||||
sa.Column('unit', sa.String(length=20), nullable=True),
|
||||
sa.Column('item_type', sa.String(length=20), nullable=True),
|
||||
sa.Column('cost_price', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('sale_price', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('min_stock', sa.Integer(), nullable=True),
|
||||
sa.Column('max_stock', sa.Integer(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_products_id'), 'products', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_products_item_type'), 'products', ['item_type'], unique=False)
|
||||
op.create_index(op.f('ix_products_sku'), 'products', ['sku'], unique=True)
|
||||
op.create_table('roles',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=50), nullable=False),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('is_system', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_roles_code'), 'roles', ['code'], unique=True)
|
||||
op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False)
|
||||
op.create_table('suppliers',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=50), nullable=True),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('contact_person', sa.String(length=100), nullable=True),
|
||||
sa.Column('phone', sa.String(length=50), nullable=True),
|
||||
sa.Column('email', sa.String(length=100), nullable=True),
|
||||
sa.Column('address', sa.Text(), nullable=True),
|
||||
sa.Column('bank_name', sa.String(length=100), nullable=True),
|
||||
sa.Column('bank_account', sa.String(length=50), nullable=True),
|
||||
sa.Column('tax_number', sa.String(length=50), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_suppliers_code'), 'suppliers', ['code'], unique=True)
|
||||
op.create_index(op.f('ix_suppliers_id'), 'suppliers', ['id'], unique=False)
|
||||
op.create_table('users',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=50), nullable=False),
|
||||
sa.Column('email', sa.String(length=255), nullable=False),
|
||||
sa.Column('hashed_password', sa.String(length=255), nullable=False),
|
||||
sa.Column('full_name', sa.String(length=100), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('last_login', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
||||
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
|
||||
op.create_table('warehouses',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=50), nullable=True),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('address', sa.Text(), nullable=True),
|
||||
sa.Column('manager', sa.String(length=100), nullable=True),
|
||||
sa.Column('phone', sa.String(length=50), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_default', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_warehouses_code'), 'warehouses', ['code'], unique=True)
|
||||
op.create_index(op.f('ix_warehouses_id'), 'warehouses', ['id'], unique=False)
|
||||
op.create_table('finance_transactions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('txn_no', sa.String(length=50), nullable=False),
|
||||
sa.Column('txn_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('partner_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('partner_id', sa.Integer(), nullable=False),
|
||||
sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False),
|
||||
sa.Column('txn_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('method', sa.String(length=30), nullable=True),
|
||||
sa.Column('account_name', sa.String(length=100), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.Column('remark', sa.Text(), nullable=True),
|
||||
sa.Column('operator_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['operator_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_finance_transactions_created_at'), 'finance_transactions', ['created_at'], unique=False)
|
||||
op.create_index(op.f('ix_finance_transactions_id'), 'finance_transactions', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_finance_transactions_partner_id'), 'finance_transactions', ['partner_id'], unique=False)
|
||||
op.create_index(op.f('ix_finance_transactions_partner_type'), 'finance_transactions', ['partner_type'], unique=False)
|
||||
op.create_index(op.f('ix_finance_transactions_status'), 'finance_transactions', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_finance_transactions_txn_date'), 'finance_transactions', ['txn_date'], unique=False)
|
||||
op.create_index(op.f('ix_finance_transactions_txn_no'), 'finance_transactions', ['txn_no'], unique=True)
|
||||
op.create_index(op.f('ix_finance_transactions_txn_type'), 'finance_transactions', ['txn_type'], unique=False)
|
||||
op.create_table('inventory',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('warehouse_id', sa.Integer(), nullable=False),
|
||||
sa.Column('quantity', sa.Numeric(precision=12, scale=4), nullable=True),
|
||||
sa.Column('locked_quantity', sa.Numeric(precision=12, scale=4), nullable=True),
|
||||
sa.Column('batch_number', sa.String(length=50), nullable=True),
|
||||
sa.Column('location', sa.String(length=100), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.CheckConstraint('quantity >= 0 AND locked_quantity >= 0 AND locked_quantity <= quantity', name='ck_inventory_qty_nonnegative'),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
|
||||
sa.ForeignKeyConstraint(['warehouse_id'], ['warehouses.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('product_id', 'warehouse_id', name='uq_inventory_product_warehouse')
|
||||
)
|
||||
op.create_index(op.f('ix_inventory_id'), 'inventory', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_inventory_product_id'), 'inventory', ['product_id'], unique=False)
|
||||
op.create_index(op.f('ix_inventory_warehouse_id'), 'inventory', ['warehouse_id'], unique=False)
|
||||
op.create_table('material_price_history',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('price', sa.Numeric(precision=12, scale=2), nullable=False),
|
||||
sa.Column('effective_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('supplier_id', sa.Integer(), nullable=True),
|
||||
sa.Column('remark', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
|
||||
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_material_price_history_effective_date'), 'material_price_history', ['effective_date'], unique=False)
|
||||
op.create_index(op.f('ix_material_price_history_id'), 'material_price_history', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_material_price_history_product_id'), 'material_price_history', ['product_id'], unique=False)
|
||||
op.create_index(op.f('ix_material_price_history_supplier_id'), 'material_price_history', ['supplier_id'], unique=False)
|
||||
op.create_table('material_suppliers',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('supplier_id', sa.Integer(), nullable=False),
|
||||
sa.Column('is_primary', sa.Boolean(), nullable=True),
|
||||
sa.Column('contact_person', sa.String(length=100), nullable=True),
|
||||
sa.Column('contact_phone', sa.String(length=50), nullable=True),
|
||||
sa.Column('lead_time', sa.Integer(), nullable=True),
|
||||
sa.Column('min_order_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
|
||||
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_material_suppliers_id'), 'material_suppliers', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_material_suppliers_product_id'), 'material_suppliers', ['product_id'], unique=False)
|
||||
op.create_index(op.f('ix_material_suppliers_supplier_id'), 'material_suppliers', ['supplier_id'], unique=False)
|
||||
op.create_table('product_materials',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('finished_product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('material_product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('quantity', sa.Numeric(precision=12, scale=4), nullable=False),
|
||||
sa.Column('loss_rate', sa.Numeric(precision=5, scale=4), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['finished_product_id'], ['products.id'], ),
|
||||
sa.ForeignKeyConstraint(['material_product_id'], ['products.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('finished_product_id', 'material_product_id', name='uq_product_material_unique')
|
||||
)
|
||||
op.create_index(op.f('ix_product_materials_finished_product_id'), 'product_materials', ['finished_product_id'], unique=False)
|
||||
op.create_index(op.f('ix_product_materials_id'), 'product_materials', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_product_materials_material_product_id'), 'product_materials', ['material_product_id'], unique=False)
|
||||
op.create_table('purchase_orders',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_no', sa.String(length=50), nullable=False),
|
||||
sa.Column('supplier_id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('expected_date', sa.Date(), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.Column('total_amount', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('paid_amount', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('remark', sa.Text(), nullable=True),
|
||||
sa.Column('operator_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('received_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('paid_date', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['operator_id'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_purchase_orders_id'), 'purchase_orders', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_purchase_orders_order_no'), 'purchase_orders', ['order_no'], unique=True)
|
||||
op.create_index(op.f('ix_purchase_orders_supplier_id'), 'purchase_orders', ['supplier_id'], unique=False)
|
||||
op.create_table('role_permissions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('role_id', sa.Integer(), nullable=False),
|
||||
sa.Column('permission_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_role_permissions_id'), 'role_permissions', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_role_permissions_permission_id'), 'role_permissions', ['permission_id'], unique=False)
|
||||
op.create_index(op.f('ix_role_permissions_role_id'), 'role_permissions', ['role_id'], unique=False)
|
||||
op.create_table('sales_orders',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_no', sa.String(length=50), nullable=False),
|
||||
sa.Column('customer_id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('delivery_date', sa.Date(), nullable=True),
|
||||
sa.Column('manufacturing_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('actual_delivery_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('actual_payment_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.Column('production_status', sa.String(length=20), nullable=True),
|
||||
sa.Column('production_no', sa.String(length=50), nullable=True),
|
||||
sa.Column('planned_material_cost', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('actual_material_cost', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('total_amount', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('received_amount', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('remark', sa.Text(), nullable=True),
|
||||
sa.Column('operator_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ),
|
||||
sa.ForeignKeyConstraint(['operator_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_sales_orders_customer_id'), 'sales_orders', ['customer_id'], unique=False)
|
||||
op.create_index(op.f('ix_sales_orders_id'), 'sales_orders', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_sales_orders_order_no'), 'sales_orders', ['order_no'], unique=True)
|
||||
op.create_index(op.f('ix_sales_orders_production_no'), 'sales_orders', ['production_no'], unique=False)
|
||||
op.create_index(op.f('ix_sales_orders_production_status'), 'sales_orders', ['production_status'], unique=False)
|
||||
op.create_table('stock_movements',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('warehouse_id', sa.Integer(), nullable=False),
|
||||
sa.Column('movement_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('quantity', sa.Numeric(precision=12, scale=4), nullable=False),
|
||||
sa.Column('before_quantity', sa.Numeric(precision=12, scale=4), nullable=True),
|
||||
sa.Column('after_quantity', sa.Numeric(precision=12, scale=4), nullable=True),
|
||||
sa.Column('reference_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('reference_id', sa.Integer(), nullable=True),
|
||||
sa.Column('reference_no', sa.String(length=50), nullable=True),
|
||||
sa.Column('unit_price', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('total_amount', sa.Numeric(precision=12, scale=2), nullable=True),
|
||||
sa.Column('remark', sa.Text(), nullable=True),
|
||||
sa.Column('operator_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['operator_id'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
|
||||
sa.ForeignKeyConstraint(['warehouse_id'], ['warehouses.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_stock_movements_created_at'), 'stock_movements', ['created_at'], unique=False)
|
||||
op.create_index(op.f('ix_stock_movements_id'), 'stock_movements', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_stock_movements_product_id'), 'stock_movements', ['product_id'], unique=False)
|
||||
op.create_table('stp_files',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('object_key', sa.String(length=500), nullable=False),
|
||||
sa.Column('storage_bucket', sa.String(length=100), nullable=False),
|
||||
sa.Column('object_url', sa.String(length=1000), nullable=True),
|
||||
sa.Column('original_filename', sa.String(length=255), nullable=False),
|
||||
sa.Column('file_size', sa.Integer(), nullable=False),
|
||||
sa.Column('file_hash', sa.String(length=64), nullable=True),
|
||||
sa.Column('mime_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('upload_batch', sa.String(length=36), nullable=True),
|
||||
sa.Column('upload_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('processed_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('volume', sa.Float(), nullable=True),
|
||||
sa.Column('surface_area', sa.Float(), nullable=True),
|
||||
sa.Column('product_weight', sa.Float(), nullable=True),
|
||||
sa.Column('file_path', sa.String(length=500), nullable=True),
|
||||
sa.Column('file_content', sa.LargeBinary(), nullable=True),
|
||||
sa.Column('filename', sa.String(length=255), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_stp_files_file_hash'), 'stp_files', ['file_hash'], unique=False)
|
||||
op.create_index(op.f('ix_stp_files_id'), 'stp_files', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_stp_files_object_key'), 'stp_files', ['object_key'], unique=False)
|
||||
op.create_index(op.f('ix_stp_files_original_filename'), 'stp_files', ['original_filename'], unique=False)
|
||||
op.create_index(op.f('ix_stp_files_status'), 'stp_files', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_stp_files_upload_batch'), 'stp_files', ['upload_batch'], unique=False)
|
||||
op.create_index(op.f('ix_stp_files_user_id'), 'stp_files', ['user_id'], unique=False)
|
||||
op.create_table('system_logs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('level', sa.String(length=20), nullable=False),
|
||||
sa.Column('message', sa.Text(), nullable=False),
|
||||
sa.Column('module', sa.String(length=100), nullable=True),
|
||||
sa.Column('function_name', sa.String(length=100), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('request_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('execution_time_ms', sa.Integer(), nullable=True),
|
||||
sa.Column('resource_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('resource_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_system_logs_created_at'), 'system_logs', ['created_at'], unique=False)
|
||||
op.create_index(op.f('ix_system_logs_id'), 'system_logs', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_system_logs_level'), 'system_logs', ['level'], unique=False)
|
||||
op.create_table('user_activities',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('activity_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('resource_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('resource_id', sa.Integer(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('meta_data', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
||||
sa.Column('user_agent', sa.String(length=500), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_user_activities_activity_type'), 'user_activities', ['activity_type'], unique=False)
|
||||
op.create_index(op.f('ix_user_activities_created_at'), 'user_activities', ['created_at'], unique=False)
|
||||
op.create_index(op.f('ix_user_activities_id'), 'user_activities', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_user_activities_user_id'), 'user_activities', ['user_id'], unique=False)
|
||||
op.create_table('user_roles',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('role_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_user_roles_id'), 'user_roles', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_user_roles_role_id'), 'user_roles', ['role_id'], unique=False)
|
||||
op.create_index(op.f('ix_user_roles_user_id'), 'user_roles', ['user_id'], unique=False)
|
||||
op.create_table('analysis_metrics',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('stp_file_id', sa.Integer(), nullable=False),
|
||||
sa.Column('volume_utilization', sa.Float(), nullable=True),
|
||||
sa.Column('topology_complexity', sa.Float(), nullable=True),
|
||||
sa.Column('wall_uniformity', sa.Float(), nullable=True),
|
||||
sa.Column('analysis_summary', sa.Text(), nullable=True),
|
||||
sa.Column('verification_status', sa.String(length=20), nullable=True),
|
||||
sa.Column('verification_volume_diff', sa.Float(), nullable=True),
|
||||
sa.Column('verification_area_diff', sa.Float(), nullable=True),
|
||||
sa.Column('verification_details', sa.JSON(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_analysis_metrics_id'), 'analysis_metrics', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_analysis_metrics_stp_file_id'), 'analysis_metrics', ['stp_file_id'], unique=False)
|
||||
op.create_table('design_recommendations',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('stp_file_id', sa.Integer(), nullable=False),
|
||||
sa.Column('rec_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('priority', sa.String(length=20), nullable=False),
|
||||
sa.Column('description', sa.String(length=500), nullable=False),
|
||||
sa.Column('reason', sa.Text(), nullable=True),
|
||||
sa.Column('parameters', sa.JSON(), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.Column('user_notes', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_design_recommendations_id'), 'design_recommendations', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_design_recommendations_stp_file_id'), 'design_recommendations', ['stp_file_id'], unique=False)
|
||||
op.create_table('finance_allocations',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('transaction_id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('order_id', sa.Integer(), nullable=False),
|
||||
sa.Column('allocated_amount', sa.Numeric(precision=12, scale=2), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['transaction_id'], ['finance_transactions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_finance_allocations_created_at'), 'finance_allocations', ['created_at'], unique=False)
|
||||
op.create_index(op.f('ix_finance_allocations_id'), 'finance_allocations', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_finance_allocations_order_id'), 'finance_allocations', ['order_id'], unique=False)
|
||||
op.create_index(op.f('ix_finance_allocations_order_type'), 'finance_allocations', ['order_type'], unique=False)
|
||||
op.create_index(op.f('ix_finance_allocations_transaction_id'), 'finance_allocations', ['transaction_id'], unique=False)
|
||||
op.create_table('geometry_data',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('stp_file_id', sa.Integer(), nullable=False),
|
||||
sa.Column('object_key', sa.String(length=500), nullable=False),
|
||||
sa.Column('storage_bucket', sa.String(length=100), nullable=False),
|
||||
sa.Column('object_url', sa.String(length=1000), nullable=True),
|
||||
sa.Column('analysis_method', sa.String(length=50), nullable=True),
|
||||
sa.Column('created_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('volume', sa.Float(), nullable=True),
|
||||
sa.Column('surface_area', sa.Float(), nullable=True),
|
||||
sa.Column('bounding_box_min', sa.JSON(), nullable=True),
|
||||
sa.Column('bounding_box_max', sa.JSON(), nullable=True),
|
||||
sa.Column('center_of_mass', sa.JSON(), nullable=True),
|
||||
sa.Column('topology_faces', sa.Integer(), nullable=True),
|
||||
sa.Column('topology_edges', sa.Integer(), nullable=True),
|
||||
sa.Column('topology_vertices', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_geometry_data_id'), 'geometry_data', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_geometry_data_stp_file_id'), 'geometry_data', ['stp_file_id'], unique=False)
|
||||
op.create_table('html_files',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('stp_file_id', sa.Integer(), nullable=False),
|
||||
sa.Column('object_key', sa.String(length=500), nullable=False),
|
||||
sa.Column('storage_bucket', sa.String(length=100), nullable=False),
|
||||
sa.Column('object_url', sa.String(length=1000), nullable=True),
|
||||
sa.Column('filename', sa.String(length=255), nullable=False),
|
||||
sa.Column('generated_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('visualization_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('has_interactive_elements', sa.Boolean(), nullable=True),
|
||||
sa.Column('file_path', sa.String(length=500), nullable=True),
|
||||
sa.Column('html_content', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_html_files_id'), 'html_files', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_html_files_stp_file_id'), 'html_files', ['stp_file_id'], unique=False)
|
||||
op.create_table('mesh_data',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('stp_file_id', sa.Integer(), nullable=False),
|
||||
sa.Column('object_key', sa.String(length=500), nullable=False),
|
||||
sa.Column('storage_bucket', sa.String(length=100), nullable=False),
|
||||
sa.Column('object_url', sa.String(length=1000), nullable=True),
|
||||
sa.Column('quality', sa.String(length=20), nullable=True),
|
||||
sa.Column('vertex_count', sa.Integer(), nullable=True),
|
||||
sa.Column('face_count', sa.Integer(), nullable=True),
|
||||
sa.Column('point_count', sa.Integer(), nullable=True),
|
||||
sa.Column('bounding_box_min', sa.JSON(), nullable=True),
|
||||
sa.Column('bounding_box_max', sa.JSON(), nullable=True),
|
||||
sa.Column('created_time', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_mesh_data_id'), 'mesh_data', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_mesh_data_stp_file_id'), 'mesh_data', ['stp_file_id'], unique=False)
|
||||
op.create_table('mold_cavity_data',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('stp_file_id', sa.Integer(), nullable=False),
|
||||
sa.Column('detailed_object_key', sa.String(length=500), nullable=False),
|
||||
sa.Column('storage_bucket', sa.String(length=100), nullable=False),
|
||||
sa.Column('mold_material', sa.String(length=100), nullable=True),
|
||||
sa.Column('mold_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('shrinkage_rate', sa.Float(), nullable=False),
|
||||
sa.Column('draft_angle', sa.Float(), nullable=False),
|
||||
sa.Column('parting_line_length', sa.Float(), nullable=True),
|
||||
sa.Column('generated_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('cavity_key_info', sa.JSON(), nullable=True),
|
||||
sa.Column('mold_size_length', sa.Float(), nullable=True),
|
||||
sa.Column('mold_size_width', sa.Float(), nullable=True),
|
||||
sa.Column('mold_size_height', sa.Float(), nullable=True),
|
||||
sa.Column('estimated_clamping_force', sa.String(length=50), nullable=True),
|
||||
sa.Column('product_weight', sa.String(length=50), nullable=True),
|
||||
sa.Column('product_volume', sa.Float(), nullable=True),
|
||||
sa.Column('wall_thickness_range', sa.String(length=50), nullable=True),
|
||||
sa.Column('complexity_score', sa.Float(), nullable=True),
|
||||
sa.Column('weld_line_risk', sa.String(length=50), nullable=True),
|
||||
sa.Column('sink_mark_risk', sa.String(length=50), nullable=True),
|
||||
sa.Column('warpage_risk', sa.String(length=50), nullable=True),
|
||||
sa.Column('best_scheme_id', sa.String(length=64), nullable=True),
|
||||
sa.Column('confidence_score', sa.Float(), nullable=True),
|
||||
sa.Column('is_fallback', sa.Boolean(), nullable=True),
|
||||
sa.Column('fallback_reason', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_mold_cavity_data_best_scheme_id'), 'mold_cavity_data', ['best_scheme_id'], unique=False)
|
||||
op.create_index(op.f('ix_mold_cavity_data_id'), 'mold_cavity_data', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_mold_cavity_data_is_fallback'), 'mold_cavity_data', ['is_fallback'], unique=False)
|
||||
op.create_index(op.f('ix_mold_cavity_data_stp_file_id'), 'mold_cavity_data', ['stp_file_id'], unique=False)
|
||||
op.create_table('processing_tasks',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('task_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('stp_file_id', sa.Integer(), nullable=False),
|
||||
sa.Column('task_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.Column('created_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('started_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('completed_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('progress', sa.Integer(), nullable=True),
|
||||
sa.Column('current_step', sa.String(length=100), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('error_stack', sa.Text(), nullable=True),
|
||||
sa.Column('parameters', sa.JSON(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_processing_tasks_id'), 'processing_tasks', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_processing_tasks_stp_file_id'), 'processing_tasks', ['stp_file_id'], unique=False)
|
||||
op.create_index(op.f('ix_processing_tasks_task_id'), 'processing_tasks', ['task_id'], unique=True)
|
||||
op.create_table('purchase_order_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('quantity', sa.Integer(), nullable=False),
|
||||
sa.Column('received_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('unit_price', sa.Numeric(precision=12, scale=2), nullable=False),
|
||||
sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False),
|
||||
sa.Column('remark', sa.Text(), nullable=True),
|
||||
sa.CheckConstraint('quantity > 0 AND received_quantity >= 0 AND received_quantity <= quantity', name='ck_purchase_order_items_qty'),
|
||||
sa.ForeignKeyConstraint(['order_id'], ['purchase_orders.id'], ),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_purchase_order_items_id'), 'purchase_order_items', ['id'], unique=False)
|
||||
op.create_table('sales_order_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('order_id', sa.Integer(), nullable=False),
|
||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||
sa.Column('quantity', sa.Integer(), nullable=False),
|
||||
sa.Column('delivered_quantity', sa.Integer(), nullable=True),
|
||||
sa.Column('unit_price', sa.Numeric(precision=12, scale=2), nullable=False),
|
||||
sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False),
|
||||
sa.Column('remark', sa.Text(), nullable=True),
|
||||
sa.CheckConstraint('quantity > 0 AND delivered_quantity >= 0 AND delivered_quantity <= quantity', name='ck_sales_order_items_qty'),
|
||||
sa.ForeignKeyConstraint(['order_id'], ['sales_orders.id'], ),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_sales_order_items_id'), 'sales_order_items', ['id'], unique=False)
|
||||
op.create_table('feature_detections',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('stp_file_id', sa.Integer(), nullable=False),
|
||||
sa.Column('feature_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('confidence', sa.Float(), nullable=False),
|
||||
sa.Column('location', sa.JSON(), nullable=True),
|
||||
sa.Column('dimensions', sa.JSON(), nullable=True),
|
||||
sa.Column('parameters', sa.JSON(), nullable=True),
|
||||
sa.Column('detected_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('geometry_data_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['geometry_data_id'], ['geometry_data.id'], ),
|
||||
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_feature_detections_feature_type'), 'feature_detections', ['feature_type'], unique=False)
|
||||
op.create_index(op.f('ix_feature_detections_id'), 'feature_detections', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_feature_detections_stp_file_id'), 'feature_detections', ['stp_file_id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_feature_detections_stp_file_id'), table_name='feature_detections')
|
||||
op.drop_index(op.f('ix_feature_detections_id'), table_name='feature_detections')
|
||||
op.drop_index(op.f('ix_feature_detections_feature_type'), table_name='feature_detections')
|
||||
op.drop_table('feature_detections')
|
||||
op.drop_index(op.f('ix_sales_order_items_id'), table_name='sales_order_items')
|
||||
op.drop_table('sales_order_items')
|
||||
op.drop_index(op.f('ix_purchase_order_items_id'), table_name='purchase_order_items')
|
||||
op.drop_table('purchase_order_items')
|
||||
op.drop_index(op.f('ix_processing_tasks_task_id'), table_name='processing_tasks')
|
||||
op.drop_index(op.f('ix_processing_tasks_stp_file_id'), table_name='processing_tasks')
|
||||
op.drop_index(op.f('ix_processing_tasks_id'), table_name='processing_tasks')
|
||||
op.drop_table('processing_tasks')
|
||||
op.drop_index(op.f('ix_mold_cavity_data_stp_file_id'), table_name='mold_cavity_data')
|
||||
op.drop_index(op.f('ix_mold_cavity_data_is_fallback'), table_name='mold_cavity_data')
|
||||
op.drop_index(op.f('ix_mold_cavity_data_id'), table_name='mold_cavity_data')
|
||||
op.drop_index(op.f('ix_mold_cavity_data_best_scheme_id'), table_name='mold_cavity_data')
|
||||
op.drop_table('mold_cavity_data')
|
||||
op.drop_index(op.f('ix_mesh_data_stp_file_id'), table_name='mesh_data')
|
||||
op.drop_index(op.f('ix_mesh_data_id'), table_name='mesh_data')
|
||||
op.drop_table('mesh_data')
|
||||
op.drop_index(op.f('ix_html_files_stp_file_id'), table_name='html_files')
|
||||
op.drop_index(op.f('ix_html_files_id'), table_name='html_files')
|
||||
op.drop_table('html_files')
|
||||
op.drop_index(op.f('ix_geometry_data_stp_file_id'), table_name='geometry_data')
|
||||
op.drop_index(op.f('ix_geometry_data_id'), table_name='geometry_data')
|
||||
op.drop_table('geometry_data')
|
||||
op.drop_index(op.f('ix_finance_allocations_transaction_id'), table_name='finance_allocations')
|
||||
op.drop_index(op.f('ix_finance_allocations_order_type'), table_name='finance_allocations')
|
||||
op.drop_index(op.f('ix_finance_allocations_order_id'), table_name='finance_allocations')
|
||||
op.drop_index(op.f('ix_finance_allocations_id'), table_name='finance_allocations')
|
||||
op.drop_index(op.f('ix_finance_allocations_created_at'), table_name='finance_allocations')
|
||||
op.drop_table('finance_allocations')
|
||||
op.drop_index(op.f('ix_design_recommendations_stp_file_id'), table_name='design_recommendations')
|
||||
op.drop_index(op.f('ix_design_recommendations_id'), table_name='design_recommendations')
|
||||
op.drop_table('design_recommendations')
|
||||
op.drop_index(op.f('ix_analysis_metrics_stp_file_id'), table_name='analysis_metrics')
|
||||
op.drop_index(op.f('ix_analysis_metrics_id'), table_name='analysis_metrics')
|
||||
op.drop_table('analysis_metrics')
|
||||
op.drop_index(op.f('ix_user_roles_user_id'), table_name='user_roles')
|
||||
op.drop_index(op.f('ix_user_roles_role_id'), table_name='user_roles')
|
||||
op.drop_index(op.f('ix_user_roles_id'), table_name='user_roles')
|
||||
op.drop_table('user_roles')
|
||||
op.drop_index(op.f('ix_user_activities_user_id'), table_name='user_activities')
|
||||
op.drop_index(op.f('ix_user_activities_id'), table_name='user_activities')
|
||||
op.drop_index(op.f('ix_user_activities_created_at'), table_name='user_activities')
|
||||
op.drop_index(op.f('ix_user_activities_activity_type'), table_name='user_activities')
|
||||
op.drop_table('user_activities')
|
||||
op.drop_index(op.f('ix_system_logs_level'), table_name='system_logs')
|
||||
op.drop_index(op.f('ix_system_logs_id'), table_name='system_logs')
|
||||
op.drop_index(op.f('ix_system_logs_created_at'), table_name='system_logs')
|
||||
op.drop_table('system_logs')
|
||||
op.drop_index(op.f('ix_stp_files_user_id'), table_name='stp_files')
|
||||
op.drop_index(op.f('ix_stp_files_upload_batch'), table_name='stp_files')
|
||||
op.drop_index(op.f('ix_stp_files_status'), table_name='stp_files')
|
||||
op.drop_index(op.f('ix_stp_files_original_filename'), table_name='stp_files')
|
||||
op.drop_index(op.f('ix_stp_files_object_key'), table_name='stp_files')
|
||||
op.drop_index(op.f('ix_stp_files_id'), table_name='stp_files')
|
||||
op.drop_index(op.f('ix_stp_files_file_hash'), table_name='stp_files')
|
||||
op.drop_table('stp_files')
|
||||
op.drop_index(op.f('ix_stock_movements_product_id'), table_name='stock_movements')
|
||||
op.drop_index(op.f('ix_stock_movements_id'), table_name='stock_movements')
|
||||
op.drop_index(op.f('ix_stock_movements_created_at'), table_name='stock_movements')
|
||||
op.drop_table('stock_movements')
|
||||
op.drop_index(op.f('ix_sales_orders_production_status'), table_name='sales_orders')
|
||||
op.drop_index(op.f('ix_sales_orders_production_no'), table_name='sales_orders')
|
||||
op.drop_index(op.f('ix_sales_orders_order_no'), table_name='sales_orders')
|
||||
op.drop_index(op.f('ix_sales_orders_id'), table_name='sales_orders')
|
||||
op.drop_index(op.f('ix_sales_orders_customer_id'), table_name='sales_orders')
|
||||
op.drop_table('sales_orders')
|
||||
op.drop_index(op.f('ix_role_permissions_role_id'), table_name='role_permissions')
|
||||
op.drop_index(op.f('ix_role_permissions_permission_id'), table_name='role_permissions')
|
||||
op.drop_index(op.f('ix_role_permissions_id'), table_name='role_permissions')
|
||||
op.drop_table('role_permissions')
|
||||
op.drop_index(op.f('ix_purchase_orders_supplier_id'), table_name='purchase_orders')
|
||||
op.drop_index(op.f('ix_purchase_orders_order_no'), table_name='purchase_orders')
|
||||
op.drop_index(op.f('ix_purchase_orders_id'), table_name='purchase_orders')
|
||||
op.drop_table('purchase_orders')
|
||||
op.drop_index(op.f('ix_product_materials_material_product_id'), table_name='product_materials')
|
||||
op.drop_index(op.f('ix_product_materials_id'), table_name='product_materials')
|
||||
op.drop_index(op.f('ix_product_materials_finished_product_id'), table_name='product_materials')
|
||||
op.drop_table('product_materials')
|
||||
op.drop_index(op.f('ix_material_suppliers_supplier_id'), table_name='material_suppliers')
|
||||
op.drop_index(op.f('ix_material_suppliers_product_id'), table_name='material_suppliers')
|
||||
op.drop_index(op.f('ix_material_suppliers_id'), table_name='material_suppliers')
|
||||
op.drop_table('material_suppliers')
|
||||
op.drop_index(op.f('ix_material_price_history_supplier_id'), table_name='material_price_history')
|
||||
op.drop_index(op.f('ix_material_price_history_product_id'), table_name='material_price_history')
|
||||
op.drop_index(op.f('ix_material_price_history_id'), table_name='material_price_history')
|
||||
op.drop_index(op.f('ix_material_price_history_effective_date'), table_name='material_price_history')
|
||||
op.drop_table('material_price_history')
|
||||
op.drop_index(op.f('ix_inventory_warehouse_id'), table_name='inventory')
|
||||
op.drop_index(op.f('ix_inventory_product_id'), table_name='inventory')
|
||||
op.drop_index(op.f('ix_inventory_id'), table_name='inventory')
|
||||
op.drop_table('inventory')
|
||||
op.drop_index(op.f('ix_finance_transactions_txn_type'), table_name='finance_transactions')
|
||||
op.drop_index(op.f('ix_finance_transactions_txn_no'), table_name='finance_transactions')
|
||||
op.drop_index(op.f('ix_finance_transactions_txn_date'), table_name='finance_transactions')
|
||||
op.drop_index(op.f('ix_finance_transactions_status'), table_name='finance_transactions')
|
||||
op.drop_index(op.f('ix_finance_transactions_partner_type'), table_name='finance_transactions')
|
||||
op.drop_index(op.f('ix_finance_transactions_partner_id'), table_name='finance_transactions')
|
||||
op.drop_index(op.f('ix_finance_transactions_id'), table_name='finance_transactions')
|
||||
op.drop_index(op.f('ix_finance_transactions_created_at'), table_name='finance_transactions')
|
||||
op.drop_table('finance_transactions')
|
||||
op.drop_index(op.f('ix_warehouses_id'), table_name='warehouses')
|
||||
op.drop_index(op.f('ix_warehouses_code'), table_name='warehouses')
|
||||
op.drop_table('warehouses')
|
||||
op.drop_index(op.f('ix_users_username'), table_name='users')
|
||||
op.drop_index(op.f('ix_users_id'), table_name='users')
|
||||
op.drop_index(op.f('ix_users_email'), table_name='users')
|
||||
op.drop_table('users')
|
||||
op.drop_index(op.f('ix_suppliers_id'), table_name='suppliers')
|
||||
op.drop_index(op.f('ix_suppliers_code'), table_name='suppliers')
|
||||
op.drop_table('suppliers')
|
||||
op.drop_index(op.f('ix_roles_id'), table_name='roles')
|
||||
op.drop_index(op.f('ix_roles_code'), table_name='roles')
|
||||
op.drop_table('roles')
|
||||
op.drop_index(op.f('ix_products_sku'), table_name='products')
|
||||
op.drop_index(op.f('ix_products_item_type'), table_name='products')
|
||||
op.drop_index(op.f('ix_products_id'), table_name='products')
|
||||
op.drop_table('products')
|
||||
op.drop_index(op.f('ix_permissions_id'), table_name='permissions')
|
||||
op.drop_index(op.f('ix_permissions_code'), table_name='permissions')
|
||||
op.drop_table('permissions')
|
||||
op.drop_index(op.f('ix_customers_id'), table_name='customers')
|
||||
op.drop_index(op.f('ix_customers_code'), table_name='customers')
|
||||
op.drop_table('customers')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,38 @@
|
||||
"""add batch_id to processing_tasks
|
||||
|
||||
批次 2(D7):批量元数据入库——processing_tasks 增加 batch_id 列,
|
||||
批量任务聚合查询走 PG,替代 Redis/进程内存中的批量元数据。
|
||||
|
||||
Revision ID: a3f8c2d91e47
|
||||
Revises: 006c18c51b0d
|
||||
Create Date: 2026-09-16
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a3f8c2d91e47'
|
||||
down_revision: Union[str, Sequence[str], None] = '006c18c51b0d'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
'processing_tasks',
|
||||
sa.Column('batch_id', sa.String(length=36), nullable=True),
|
||||
)
|
||||
op.create_index(
|
||||
'ix_processing_tasks_batch_id',
|
||||
'processing_tasks',
|
||||
['batch_id'],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index('ix_processing_tasks_batch_id', table_name='processing_tasks')
|
||||
op.drop_column('processing_tasks', 'batch_id')
|
||||
@@ -0,0 +1,93 @@
|
||||
"""add experience_feedback
|
||||
|
||||
Human-in-Loop 闭环(D17,2026-09):老师傅对系统推荐方案给出'采纳 / 调整 / 拒绝'
|
||||
反馈,按'产品指纹 + 工艺参数'为索引跨任务匹配;下次同指纹产品分析自动消费。
|
||||
- 新增 experience_feedback 表(方案级反馈)
|
||||
- 跨模块裸 FK(user_id / processing_task_id / stp_file_id)
|
||||
- 复合索引 (stp_file_id, scheme_axis, feedback_status) 用于按方向聚合采纳计数
|
||||
- GIN 索引(PG only)用于 fingerprint JSON 字段检索
|
||||
|
||||
Revision ID: b7d1f4a92c3e
|
||||
Revises: a3f8c2d91e47
|
||||
Create Date: 2026-09-22
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b7d1f4a92c3e'
|
||||
down_revision: Union[str, Sequence[str], None] = 'a3f8c2d91e47'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
'experience_feedback',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('processing_task_id', sa.Integer(), nullable=False),
|
||||
sa.Column('stp_file_id', sa.Integer(), nullable=False),
|
||||
sa.Column('scheme_id', sa.String(length=64), nullable=False),
|
||||
sa.Column('scheme_axis', sa.String(length=1), nullable=False),
|
||||
sa.Column('scheme_method', sa.String(length=50), nullable=True),
|
||||
sa.Column('feedback_status', sa.String(length=20), nullable=False),
|
||||
sa.Column('feedback_reason', sa.Text(), nullable=True),
|
||||
sa.Column('adjust_suggestion', sa.Text(), nullable=True),
|
||||
sa.Column('process_params_snapshot', sa.JSON(), nullable=True),
|
||||
sa.Column('fingerprint', sa.JSON(), nullable=False),
|
||||
sa.Column('confidence_at_submit', sa.Float(), nullable=True),
|
||||
sa.Column('score_at_submit', sa.Float(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('role_code', sa.String(length=50), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('expires_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.ForeignKeyConstraint(
|
||||
['processing_task_id'], ['processing_tasks.id'], ondelete='CASCADE'
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['stp_file_id'], ['stp_files.id'], ondelete='CASCADE'
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['user_id'], ['users.id'], ondelete='RESTRICT'
|
||||
),
|
||||
)
|
||||
op.create_index(op.f('ix_experience_feedback_id'), 'experience_feedback', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_experience_feedback_processing_task_id'), 'experience_feedback', ['processing_task_id'], unique=False)
|
||||
op.create_index(op.f('ix_experience_feedback_stp_file_id'), 'experience_feedback', ['stp_file_id'], unique=False)
|
||||
op.create_index(op.f('ix_experience_feedback_scheme_id'), 'experience_feedback', ['scheme_id'], unique=False)
|
||||
op.create_index(op.f('ix_experience_feedback_feedback_status'), 'experience_feedback', ['feedback_status'], unique=False)
|
||||
op.create_index(op.f('ix_experience_feedback_user_id'), 'experience_feedback', ['user_id'], unique=False)
|
||||
op.create_index(op.f('ix_experience_feedback_created_at'), 'experience_feedback', ['created_at'], unique=False)
|
||||
op.create_index(op.f('ix_experience_feedback_expires_at'), 'experience_feedback', ['expires_at'], unique=False)
|
||||
op.create_index(
|
||||
op.f('ix_experience_feedback_stp_axis_status'),
|
||||
'experience_feedback',
|
||||
['stp_file_id', 'scheme_axis', 'feedback_status'],
|
||||
unique=False,
|
||||
)
|
||||
# 注意:此处原本计划给 fingerprint 建 GIN 索引("jsonb_path_query 类查询"),
|
||||
# 但该列是 sa.JSON() → PG 的 json 类型,而 GIN 只支持 jsonb(json 无默认
|
||||
# 操作符类,CREATE INDEX 直接报 UndefinedObject)。且代码侧并无 JSON 包含
|
||||
# 查询——聚合过滤在 Python 侧进行,DB 侧走上方 (stp_file_id, scheme_axis,
|
||||
# feedback_status) 复合索引。故不建此索引;未来若真需要 JSON 检索,应先把
|
||||
# 列迁为 jsonb 再建 GIN。首次对生产库执行时曾因此报错回滚(2026-09-26)。
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
if op.get_bind().dialect.name == 'postgresql':
|
||||
op.execute("DROP INDEX IF EXISTS ix_experience_feedback_fingerprint_gin")
|
||||
op.drop_index(op.f('ix_experience_feedback_stp_axis_status'), table_name='experience_feedback')
|
||||
op.drop_index(op.f('ix_experience_feedback_expires_at'), table_name='experience_feedback')
|
||||
op.drop_index(op.f('ix_experience_feedback_created_at'), table_name='experience_feedback')
|
||||
op.drop_index(op.f('ix_experience_feedback_user_id'), table_name='experience_feedback')
|
||||
op.drop_index(op.f('ix_experience_feedback_feedback_status'), table_name='experience_feedback')
|
||||
op.drop_index(op.f('ix_experience_feedback_scheme_id'), table_name='experience_feedback')
|
||||
op.drop_index(op.f('ix_experience_feedback_stp_file_id'), table_name='experience_feedback')
|
||||
op.drop_index(op.f('ix_experience_feedback_processing_task_id'), table_name='experience_feedback')
|
||||
op.drop_index(op.f('ix_experience_feedback_id'), table_name='experience_feedback')
|
||||
op.drop_table('experience_feedback')
|
||||
File diff suppressed because it is too large
Load Diff
+9625
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
asyncio_mode = auto
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user