diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..56a9a18 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,67 @@ +# ============================================ +# .dockerignore — 排除无关文件,加速构建 +# ============================================ + +# Python +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +.eggs/ +dist/ +build/ + +# Virtual environments +.env +venv/ +env/ +.conda/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Git +.git/ +.gitignore +.gitattributes + +# 前端(仅在需要构建时包含) +frontend/node_modules/ +frontend/dist/ + +# 测试 +tests/ +.pytest_cache/ +.coverage +htmlcov/ + +# 文档 +docs/ +*.md +README* + +# 临时文件 +*.log +logs/ +temp/ +tmp/ + +# Docker +Dockerfile* +docker-compose*.yml +deploy/ + +# 其他 +.trae/ +scripts/ +pip_audit_local.json +temp_requirements_audit.txt +local_src.txt +git_src.txt +start.sh +CHANGELOG.md +LICENSE diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..424698a --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,64 @@ +# ============================================ +# 环境变量模板 — 独立部署版 +# ============================================ +# 复制为 .env 并修改配置 + +# 数据库 +DB_HOST=postgres +DB_PORT=5432 +DB_NAME=moldinsight +DB_USER=moldinsight_user +DB_PASSWORD=moldinsight_password + +# Redis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= + +# MinIO (RustFS 兼容存储) +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=minioadmin + +# 认证 +SECRET_KEY=your-secret-key-change-in-production-min-32-chars +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=1440 + +# 管理员 +ADMIN_USERNAME=admin +ADMIN_PASSWORD=change-this-to-a-secure-password +ADMIN_EMAIL=admin@gemold.com +ADMIN_FULL_NAME=系统管理员 + +# 端口 +MOLDINSIGHT_PORT=8000 +INVENTORY_PORT=8001 + +# 文件上传 +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_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 diff --git a/deploy/Dockerfile.base b/deploy/Dockerfile.base new file mode 100644 index 0000000..cc66c7b --- /dev/null +++ b/deploy/Dockerfile.base @@ -0,0 +1,17 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* + +COPY deploy/requirements-base.txt . + +RUN pip install --no-cache-dir -r requirements-base.txt && \ + rm requirements-base.txt + +COPY src/shared/ /app/src/shared/ + +ENV PYTHONPATH=/app/src +ENV PYTHONUNBUFFERED=1 diff --git a/deploy/Dockerfile.celery b/deploy/Dockerfile.celery new file mode 100644 index 0000000..31b2f4f --- /dev/null +++ b/deploy/Dockerfile.celery @@ -0,0 +1,3 @@ +FROM gemold-moldinsight:latest + +CMD ["celery", "-A", "celery_app", "worker", "--workdir=/app/src", "--concurrency=2", "--loglevel=info"] diff --git a/deploy/Dockerfile.inventory b/deploy/Dockerfile.inventory new file mode 100644 index 0000000..0f014b9 --- /dev/null +++ b/deploy/Dockerfile.inventory @@ -0,0 +1,14 @@ +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 + +HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=30s \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8001/health')" || exit 1 + +CMD ["python", "-m", "uvicorn", "entrypoints.inventory:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/deploy/Dockerfile.moldinsight b/deploy/Dockerfile.moldinsight new file mode 100644 index 0000000..76515a7 --- /dev/null +++ b/deploy/Dockerfile.moldinsight @@ -0,0 +1,30 @@ +FROM continuumio/miniconda3:latest AS pythonocc + +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 + +FROM gemold-base:latest + +COPY --from=pythonocc /opt/conda/envs/moldinsight/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/ + +COPY deploy/requirements-moldinsight.txt . + +RUN pip install --no-cache-dir -r requirements-moldinsight.txt && \ + rm requirements-moldinsight.txt + +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/ +COPY uploads/ /app/uploads/ +COPY static/ /app/static/ +COPY html_output/ /app/html_output/ + +RUN mkdir -p /app/logs + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=60s \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 + +CMD ["python", "-m", "uvicorn", "entrypoints.moldinsight:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/deploy/build.bat b/deploy/build.bat new file mode 100644 index 0000000..0e54ead --- /dev/null +++ b/deploy/build.bat @@ -0,0 +1,32 @@ +@echo off +REM ============================================ +REM 构建脚本 — 独立部署 (Windows) +REM ============================================ +cd /d "%~dp0\.." + +echo === 构建基础镜像 === +docker build -t gemold-base:latest -f deploy\Dockerfile.base . + +echo. +echo === 构建 MoldInsight 镜像 (含 PythonOCC) === +docker build -t gemold-moldinsight:latest -f deploy\Dockerfile.moldinsight . + +echo. +echo === 构建 Inventory 镜像 === +docker build -t gemold-inventory:latest -f deploy\Dockerfile.inventory . + +echo. +echo === 构建 Celery Worker 镜像 === +docker build -t gemold-celery:latest -f deploy\Dockerfile.celery . + +echo. +echo === 全部构建完成 === +echo. +echo 启动完整系统: +echo cd deploy ^&^& docker compose --profile full up -d +echo. +echo 仅启动进销存: +echo cd deploy ^&^& docker compose --profile inventory up -d +echo. +echo 仅启动模具分析: +echo cd deploy ^&^& docker compose --profile moldinsight up -d diff --git a/deploy/build.sh b/deploy/build.sh new file mode 100644 index 0000000..21a9ec3 --- /dev/null +++ b/deploy/build.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# ============================================ +# 构建脚本 — 独立部署 +# ============================================ +set -e + +cd "$(dirname "$0")/.." + +echo "=== 构建基础镜像 ===" +docker build -t gemold-base:latest -f deploy/Dockerfile.base . + +echo "" +echo "=== 构建 MoldInsight 镜像 (含 PythonOCC) ===" +docker build -t gemold-moldinsight:latest -f deploy/Dockerfile.moldinsight . + +echo "" +echo "=== 构建 Inventory 镜像 ===" +docker build -t gemold-inventory:latest -f deploy/Dockerfile.inventory . + +echo "" +echo "=== 构建 Celery Worker 镜像 ===" +docker build -t gemold-celery:latest -f deploy/Dockerfile.celery . + +echo "" +echo "=== 全部构建完成 ===" +echo "" +echo "启动完整系统:" +echo " cd deploy && docker compose --profile full up -d" +echo "" +echo "仅启动进销存:" +echo " cd deploy && docker compose --profile inventory up -d" +echo "" +echo "仅启动模具分析:" +echo " cd deploy && docker compose --profile moldinsight up -d" diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..f27791d --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,163 @@ +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" + RUSTFS_ENDPOINT: http://minio:9000 + RUSTFS_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin} + RUSTFS_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin} + 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 diff --git a/deploy/requirements-base.txt b/deploy/requirements-base.txt new file mode 100644 index 0000000..9fb8789 --- /dev/null +++ b/deploy/requirements-base.txt @@ -0,0 +1,30 @@ +-i https://mirrors.aliyun.com/pypi/simple/ +--trusted-host mirrors.aliyun.com + +fastapi>=0.100.0 +uvicorn[standard]>=0.22.0 +pydantic>=2.0.0 +python-multipart>=0.0.6 + +sqlalchemy>=2.0.0 +psycopg2-binary>=2.9.0 +asyncpg>=0.28.0 +alembic>=1.11.0 + +python-jose[cryptography]>=3.3.0 +bcrypt>=4.0.0 +passlib>=1.7.4 +email-validator>=2.0.0 + +aiofiles>=23.0.0 +orjson>=3.9.0 +python-dotenv>=1.0.0 +jinja2>=3.1.0 +Pillow>=12.2.0 +pyyaml>=6.0 +python-dateutil>=2.8.0 + +loguru>=0.7.0 + +httpx>=0.24.0 +redis>=4.5.0 diff --git a/deploy/requirements-moldinsight.txt b/deploy/requirements-moldinsight.txt new file mode 100644 index 0000000..c8863da --- /dev/null +++ b/deploy/requirements-moldinsight.txt @@ -0,0 +1,20 @@ +-i https://mirrors.aliyun.com/pypi/simple/ +--trusted-host mirrors.aliyun.com + +trimesh>=3.21.0 +numpy>=1.24.0 +scipy>=1.10.0 +pyvista>=0.38.0 + +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 +aiosqlite>=0.19.0 +black>=23.0.0 +flake8>=6.0.0 +mypy>=1.0.0 diff --git a/src/entrypoints/__init__.py b/src/entrypoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/entrypoints/inventory.py b/src/entrypoints/inventory.py new file mode 100644 index 0000000..abec5b6 --- /dev/null +++ b/src/entrypoints/inventory.py @@ -0,0 +1,78 @@ +import os, sys +from pathlib import Path + +src_root = Path(__file__).parent.parent +sys.path.insert(0, str(src_root)) + +os.chdir(Path(__file__).parent.parent.parent) + +from fastapi import FastAPI, Request +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, FileResponse +import asyncio, time + +from shared.config.settings import settings +from shared.services.auth_routes import router as auth_router +from shared.utils.logger import setup_logging, get_logger +from shared.database.init_db import init_database + +setup_logging() +logger = get_logger(__name__) + +app = FastAPI(title="Gemold - 进销存管理系统", version="4.0.0") + +app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) + +@app.middleware("http") +async def log_requests(request: Request, call_next): + start_time = time.time() + response = await call_next(request) + duration = time.time() - start_time + if response.status_code >= 400: + logger.warning(f"[HTTP] {request.method} {request.url.path} -> {response.status_code} ({duration:.2f}s)") + return response + +@app.on_event("startup") +async def startup_event(): + success = await init_database(keep_connected=True) + print(f"[{'OK' if success else 'FAIL'}] 数据库初始化") + try: + from shared.services.redis_task_manager import redis_task_manager + await redis_task_manager.connect() + print(f"[{'OK' if redis_task_manager.is_connected else 'WARN'}] Redis") + except Exception as e: + print(f"[WARN] Redis异常: {e}") + +@app.on_event("shutdown") +async def shutdown_event(): + try: + from shared.services.redis_task_manager import redis_task_manager + await redis_task_manager.disconnect() + except: pass + +app.mount("/static", StaticFiles(directory=os.path.join(os.getcwd(), "static")), name="static") + +app.include_router(auth_router) + +try: + from inventory.api import inventory_router + app.include_router(inventory_router) +except Exception as e: + print(f"[WARN] Inventory 路由: {e}") + +@app.get("/health") +async def health(): + from shared.database.database import db_manager + from sqlalchemy import text + db_ok = False; db_error = None + try: + if not db_manager.is_connected: await db_manager.connect() + async with db_manager.engine.begin() as conn: await conn.execute(text("SELECT 1")) + db_ok = True + except Exception as e: db_error = str(e) + return {"status": "healthy", "service": "inventory", "version": "4.0.0", "database_connected": db_ok, "database_error": db_error} + +@app.get("/{full_path:path}") +async def spa_fallback(full_path: str): + return FileResponse(os.path.join(os.getcwd(), "static", "index.html")) diff --git a/src/entrypoints/moldinsight.py b/src/entrypoints/moldinsight.py new file mode 100644 index 0000000..44e013c --- /dev/null +++ b/src/entrypoints/moldinsight.py @@ -0,0 +1,88 @@ +import os, sys +from pathlib import Path + +src_root = Path(__file__).parent.parent +sys.path.insert(0, str(src_root)) + +os.chdir(Path(__file__).parent.parent.parent) + +from fastapi import FastAPI, Request +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, FileResponse +import asyncio, time + +from shared.config.settings import settings +from shared.services.auth_routes import router as auth_router +from shared.utils.logger import setup_logging, get_logger +from shared.database.init_db import init_database + +setup_logging() +logger = get_logger(__name__) + +app = FastAPI(title="Gemold - 模具分析引擎", version="4.0.0") + +app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) + +@app.middleware("http") +async def log_requests(request: Request, call_next): + start_time = time.time() + response = await call_next(request) + duration = time.time() - start_time + if response.status_code >= 400: + logger.warning(f"[HTTP] {request.method} {request.url.path} -> {response.status_code} ({duration:.2f}s)") + return response + +@app.on_event("startup") +async def startup_event(): + success = await init_database(keep_connected=True) + print(f"[{'OK' if success else 'FAIL'}] 数据库初始化") + try: + from moldinsight.storage.rustfs_storage import rustfs_manager + await rustfs_manager.connect(endpoint=settings.RUSTFS_ENDPOINT, access_key=settings.RUSTFS_ACCESS_KEY, secret_key=settings.RUSTFS_SECRET_KEY, timeout=settings.RUSTFS_TIMEOUT) + print("[OK] RustFS连接成功") + except Exception as e: + print(f"[WARN] RustFS连接失败: {e}") + try: + from shared.services.redis_task_manager import redis_task_manager + await redis_task_manager.connect() + print(f"[{'OK' if redis_task_manager.is_connected else 'WARN'}] Redis") + except Exception as e: + print(f"[WARN] Redis异常: {e}") + +@app.on_event("shutdown") +async def shutdown_event(): + try: + from shared.services.redis_task_manager import redis_task_manager + await redis_task_manager.disconnect() + except: pass + +UPLOAD_DIR = Path("uploads"); UPLOAD_DIR.mkdir(exist_ok=True) +Path("html_output").mkdir(exist_ok=True) + +app.mount("/static", StaticFiles(directory=os.path.join(os.getcwd(), "static")), name="static") +app.mount("/html", StaticFiles(directory=os.path.join(os.getcwd(), "html_output")), name="html") + +app.include_router(auth_router) + +try: + from moldinsight.api import router as moldinsight_router + app.include_router(moldinsight_router, prefix="/api") +except Exception as e: + print(f"[WARN] MoldInsight 路由: {e}") + +@app.get("/health") +async def health(): + from shared.database.database import db_manager + from sqlalchemy import text + db_ok = False; db_error = None + try: + if not db_manager.is_connected: await db_manager.connect() + async with db_manager.engine.begin() as conn: await conn.execute(text("SELECT 1")) + db_ok = True + except Exception as e: db_error = str(e) + return {"status": "healthy", "service": "moldinsight", "version": "4.0.0", "database_connected": db_ok, "database_error": db_error} + +@app.get("/{full_path:path}") +async def spa_fallback(full_path: str): + return FileResponse(os.path.join(os.getcwd(), "static", "index.html"))