diff --git a/CHANGE.log.md b/CHANGE.log.md
new file mode 100644
index 0000000..399f72c
--- /dev/null
+++ b/CHANGE.log.md
@@ -0,0 +1,107 @@
+# 更新日志 (Changelog)
+
+
+## [4.0.0] - 2026-03-07
+
+### 新增功能 ✨
+
+#### 前端重构
+- **白色简约现代UI** - 完全重构前端为白色简约现代化风格
+ - 緔洁的白色背景配色
+ - 緩和阴影和圆角设计
+ - 娡块化CSS变量系统
+ - 响应式布局优化
+
+#### 3D可视化
+- **PythonOCC点云模型** - 从STP文件提取真实点云数据生成3D模型
+- **模具型腔可视化** - 3D预览显示型腔、型芯、分型面
+- **交互式控制** - 支持旋转、缩放、平移操作
+- **视图切换** - 显示/隐藏产品、模具、分型面、点云
+
+#### 验证工具
+- **STP验证脚本** - 使用FreeCAD命令行验证几何数据准确性
+ - 对比FreeCAD和PythonOCC的测量结果
+ - 生成详细的验证报告
+
+#### 分析功能
+- **产品特征识别** - 检测壁厚、加强筋、孔洞、倒扣等特征
+- **泡沫包装设计决策** - 噃度、布局、定位结构建议
+- **模具工程决策** - 型腔数量、模架尺寸、材料选择
+- **质量指标** - 体积利用率、拓扑复杂度、壁厚均匀性
+
+### 优化改进 🔧
+
+#### 数据处理
+- **点云数据提取优化** - 修复PythonOCC网格提取的正确实现
+- **数据持久化** - 添加AnalysisMetrics表存储分析指标
+- **API响应优化** - 修复字段名不匹配问题
+
+#### 用户体验
+- **分析摘要显示** - 在结果页面顶部显示分析摘要
+- **设计建议列表** - 按优先级排序显示所有建议
+- **关键信息完善** - 显示更多模具参数和制造要求
+
+### 问题修复 🐛
+
+- 修复 `getPriorityText` 函数未定义错误
+- 修复 `controls` 变量初始化顺序错误
+- 修复 HTML生成器JavaScript语法错误
+- 修复历史记录API路径重复前缀问题
+- 修复点云数据传递逻辑错误
+
+---
+
+## [3.0.0] - 2026-03-06
+
+### 新增功能 ✨
+
+#### 用户系统
+- **用户认证** - JWT令牌认证机制
+- **角色权限** - 基于角色的访问控制
+- **管理员账户** - 支持环境变量配置管理员
+
+#### 进销存模块
+- **仪表盘** - 库存概览和统计数据
+- **产品管理** - 产品CRUD操作
+- **库存预警** - 低库存提醒功能
+
+#### 项目架构
+- **新主页** - Gemold官网主页
+- **模块化路由** - 分离认证、进销存、分析模块
+
+### 优化改进 🔧
+
+- **UI主题** - 从深色主题改为现代蓝紫色渐变
+- **按钮样式** - 修复按钮大小和可见性问题
+- **数据库连接** - 优化异步会话管理
+
+---
+
+## [2.0.0] - 2026-03-05
+
+### 新增功能 ✨
+
+#### 核心分析
+- **STP文件解析** - 使用PythonOCC解析STP文件
+- **几何分析** - 计算体积、表面积、边界框等
+- **模具设计建议** - 自动生成设计建议
+
+#### Web界面
+- **文件上传** - 拖拽上传支持
+- **任务状态** - 实时进度显示
+- **结果展示** - 分析结果可视化
+
+#### 数据存储
+- **PostgreSQL集成** - 数据持久化存储
+- **RustFS集成** - 文件存储服务
+
+---
+
+## [1.0.0] - 2026-03-04
+
+### 初始版本 🎉
+
+- 项目初始化
+- 基础架构搭建
+- STP文件解析功能
+- 基础Web界面
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..7f45aa9
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,104 @@
+# 更新日志 (Changelog)
+
+本文档记录 geMoldInsight 项目的所有重要变更。
+
+格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
+版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
+
+---
+
+## [Unreleased]
+
+### 计划中
+- 支持更多CAD格式(IGES、BREP)
+- 模具成本估算模块
+- 批量分析功能
+- API文档自动生成
+
+---
+
+## [4.0.0] - 2026-03-07
+
+### 新增
+- **白色简约现代UI** - 完全重构前端为白色简约现代化风格
+- **PythonOCC点云3D模型** - 从STP文件提取真实点云数据生成3D模型
+- **模具型腔可视化** - 3D预览显示型腔、型芯、分型面
+- **STP验证脚本** - 使用FreeCAD命令行验证几何数据准确性
+- **PyCharm运行配置** - 添加IDE一键启动支持
+- **Windows启动脚本** - 添加Windows平台启动脚本
+
+### 优化
+- **网格生成算法** - 优化PythonOCC网格提取,支持位置变换
+- **点云采样** - 动态调整采样点数,优化渲染性能
+- **相机自动对准** - 根据点云边界框自动调整相机位置
+
+### 修复
+- 修复 `getPriorityText` 函数未定义错误
+- 修复 `controls` 变量初始化顺序错误
+- 修复历史记录API路由重复前缀问题
+- 修复点云数据结构不匹配问题
+- 修复HTML生成器JavaScript语法错误
+
+---
+
+## [3.0.0] - 2026-03-06
+
+### 新增
+- **用户权限管理系统** - 完整的角色权限控制
+- **管理员账户** - 支持环境变量配置管理员
+- **进销存模块** - 基础库存管理功能
+- **Gemold主页** - 新增系统主页
+
+### 优化
+- **UI主题** - 从深色改为现代蓝紫色渐变主题
+- **数据库模型** - 添加用户、角色、权限表
+
+### 修复
+- 修复数据库会话管理问题
+- 修复bcrypt版本兼容性问题
+- 修复密码长度限制错误
+- 修复email-validator缺失问题
+
+---
+
+## [2.0.0] - 2026-03-05
+
+### 新增
+- **分析结果详情** - 产品特征识别、泡沫包装设计、模具工程决策
+- **质量指标显示** - 体积利用率、拓扑复杂度、壁厚均匀性
+- **设计建议列表** - 按优先级排序的完整建议
+
+### 优化
+- **数据持久化** - 分析结果保存到数据库
+- **API响应结构** - 统一数据返回格式
+
+### 修复
+- 修复分析结果字段名不匹配问题
+- 修复特征类型字段名错误
+
+---
+
+## [1.0.0] - 2026-03-04
+
+### 新增
+- **STP文件上传** - 支持拖拽上传
+- **几何分析** - 体积、表面积、边界框计算
+- **模具型腔生成** - 自动生成型腔数据
+- **3D可视化** - HTML格式3D预览
+- **任务状态跟踪** - 实时进度显示
+- **PostgreSQL存储** - 数据持久化
+- **RustFS集成** - 文件存储服务
+
+### 技术栈
+- **后端**: FastAPI + PythonOCC + SQLAlchemy
+- **前端**: Vue.js 3 + Three.js
+- **数据库**: PostgreSQL
+- **存储**: RustFS (S3兼容)
+
+---
+
+## 版本说明
+
+- **[Major]**: 重大架构变更或不兼容的API修改
+- **[Minor]**: 新增功能,向后兼容
+- **[Patch]**: Bug修复,向后兼容
diff --git a/README.md b/README.md
index 5c2b294..fe2255c 100644
--- a/README.md
+++ b/README.md
@@ -1,77 +1,294 @@
-# MoldInsight - 模具几何分析系统
+# Gemold - 模具制造管理系统
+
+
+
+
+
+
+
+
+
+
+
+
+## 项目简介
+
+**Gemold** 是一个面向模具制造行业的综合性管理系统,集成了STP文件分析、模具设计建议、用户权限管理和进销存功能。 系统采用现代化的技术栈,提供高效、稳定、易扩展的解决方案。
+
+### 核心功能
+
+| 功能模块 | 描述 |
+|---------|------|
+| 🔷 STP文件分析 | 使用PythonOCC解析STP文件,提取几何特征 |
+| 📊 模具设计建议 | 自动生成型腔、型芯、工艺参数等设计方案 |
+| 🎨 3D可视化 | 实时预览产品模型和模具结构 |
+| 👥 用户权限管理 | 完整的角色权限控制系统 |
+| 📦 进销存管理 | 库存、供应商、客户管理 |
+| 💾 数据持久化 | PostgreSQL + RustFS存储 |
+
+---
+
+## 技术栈
+
+### 后端
+- **FastAPI** - 笷性能异步Web框架
+- **PythonOCC** - 专业CAD几何处理
+- **SQLAlchemy** - ORM框架
+- **PostgreSQL** - 关系型数据库
+- **RustFS** - 高性能文件存储
+
+### 前端
+- **Vue.js 3** - 渐进式JavaScript框架
+- **Three.js** - 3D可视化库
+- **原生CSS** - 白色简约现代风格
+
+### 基础设施
+- **Docker** - 容器化部署
+- **Systemd** - 服务管理
+- **Conda** - 环境管理
+
+---
## 项目结构
```
-moldinsight_project/
+geMoldInsight/
├── src/ # 源代码目录
│ ├── main.py # 主程序入口
│ ├── api/ # API路由
-│ │ └── routes.py
+│ │ ├── routes.py # 主API路由
+│ │ ├── auth_routes.py # 认证API
+│ │ └── inventory_routes.py # 进销存API
│ ├── core/ # 核心业务逻辑
-│ │ ├── stp_parser.py
-│ │ ├── geometry_analyzer.py
-│ │ └── mesh_generator.py
+│ │ ├── stp_parser.py # STP文件解析
+│ │ ├── geometry_analyzer.py # 几何分析
+│ │ ├── mesh_generator.py # 网格生成
+│ │ └── mold_generator.py # 模具生成
│ ├── models/ # 数据模型
-│ │ ├── schemas.py
-│ │ └── database.py
-│ ├── services/ # 业务服务
-│ │ └── storage_service.py
+│ │ ├── database.py # 数据库模型
+│ │ └── schemas.py # 数据模式
+│ ├── services/ # 服务层
+│ │ ├── auth_service.py # 认证服务
+│ │ ├── storage_service.py # 存储服务
+│ │ └── storage_integration_rustfs.py # RustFS集成
│ ├── database/ # 数据库管理
-│ │ ├── database.py
-│ │ └── init_db.py
+│ │ ├── database.py # 数据库连接
+│ │ └── init_db.py # 数据库初始化
│ └── utils/ # 工具类
-│ ├── logger.py
-│ ├── file_handler.py
-│ └── html_generator.py
-├── config/ # 配置文件
-│ └── settings.py
+│ ├── logger.py # 日志工具
+│ ├── file_handler.py # 文件处理
+│ └── html_generator.py # HTML生成
├── static/ # 静态文件
-│ ├── script.js
-│ └── style.css
-├── templates/ # HTML模板
-│ └── index.html
+│ ├── vue-app.js # Vue应用
+│ ├── style.css # 样式文件
+│ └── index.html # 主页面
+├── scripts/ # 脚本工具
+│ └── verify_stp.py # STP验证脚本
├── uploads/ # 上传文件目录
├── html_output/ # 生成的HTML文件
├── logs/ # 日志文件
-├── tests/ # 测试文件
-├── docs/ # 文档
├── requirements.txt # 依赖包
├── docker-compose.yml # Docker配置
+├── start.sh # Linux启动脚本
+├── start.bat # Windows启动脚本
└── .env # 环境变量
```
-## 功能特性
-
-- ✅ STP文件解析和几何分析
-- ✅ JSON数据导出
-- ✅ PostgreSQL数据库存储
-- ✅ 3D可视化HTML生成
-- ✅ Web界面文件上传
-- ✅ 任务状态跟踪
-
-## 这是一个模具设计项目:
-
-- 输入: 产品的三维模型(STP文件)
-- 输出: 模具设计方案(型腔、型芯、工艺参数等)
-- 目标: 为泡沫产品(ABS、PP、PC等材料)设计铝制模具
+---
## 快速开始
-1. 安装依赖:
+### 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. 配置环境变量
-2. 配置数据库连接(修改.env文件)
+创建 `.env` 文件:
-3. 启动服务:
-```bash
-python src/main.py
+```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. 访问服务
- - 查看端口配置:打开 `.env` 文件查看 `PORT` 的值
- - 默认端口:8000
- - 访问地址:http://localhost:${PORT}
\ No newline at end of file
+### 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: 测试相关
+
+---
+
+## 许可证
+
+本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件
+
+---
+
+## 贡献者
+
+感谢所有为这个项目做出贡献的开发者。
+
+---
+
+## 联系方式
+
+- 项目地址: [GitHub](https://github.com/your-org/gemold)
+- 问题反馈: [Issues](https://github.com/your-org/gemold/issues)
diff --git a/requirements.txt b/requirements.txt
index c6e0500..a04bfd2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,43 +1,84 @@
-# requirements.txt
-# 几何处理核心
+# ============================================
+# geMoldInsight 项目依赖
+# ============================================
+
+# 使用阿里云镜像加速
-i https://mirrors.aliyun.com/pypi/simple/
--trusted-host mirrors.aliyun.com
-# pythonocc-core==7.7.0
-pyvista
-trimesh
-numpy
-scipy
-dotenv
-fastapi
-numpy
-OCC
+
+# ============================================
+# 几何处理核心
+# ============================================
+# PythonOCC - CAD几何处理
+pythonocc-core>=7.7.0
+# 网格处理
+trimesh>=3.21.0
+# 科学计算
+numpy>=1.24.0
+scipy>=1.10.0
+# 3D可视化
+pyvista>=0.38.0
+
+# ============================================
# Web框架
-fastapi
-uvicorn
-pydantic
-python-multipart
+# ============================================
+fastapi>=0.100.0
+uvicorn[standard]>=0.22.0
+pydantic>=2.0.0
+python-multipart>=0.0.6
+# ============================================
# 数据库
-sqlalchemy
-psycopg2-binary
-asyncpg
+# ============================================
+sqlalchemy>=2.0.0
+psycopg2-binary>=2.9.0
+asyncpg>=0.28.0
+alembic>=1.11.0
-# 对象存储 (RustFS S3v4)
-minio
-aiohttp
+# ============================================
+# 对象存储
+# ============================================
+minio>=7.1.0
+aiohttp>=3.8.0
+# ============================================
# 消息队列
-kafka-python
-redis
+# ============================================
+kafka-python>=2.0.2
+redis>=4.5.0
+# ============================================
+# 认证与安全
+# ============================================
+python-jose[cryptography]>=3.3.0
+bcrypt>=4.0.0
+passlib>=1.7.4
+email-validator>=2.0.0
+
+# ============================================
# 工具库
-aiofiles
-python-dotenv
-jinja2
+# ============================================
+aiofiles>=23.0.0
+python-dotenv>=1.0.0
+jinja2>=3.1.0
+pyyaml>=6.0
+python-dateutil>=2.8.0
-# JWT认证
-python-jose[cryptography]
-bcrypt
+# ============================================
+# 日志与监控
+# ============================================
+loguru>=0.7.0
-# 邮箱验证
-email-validator
+# ============================================
+# 测试工具
+# ============================================
+pytest>=7.0.0
+pytest-asyncio>=0.21.0
+httpx>=0.24.0
+
+# ============================================
+# 开发工具
+# ============================================
+black>=23.0.0
+flake8>=6.0.0
+mypy>=1.0.0
diff --git a/scripts/migrate_multi_upload.py b/scripts/migrate_multi_upload.py
new file mode 100644
index 0000000..73bd628
--- /dev/null
+++ b/scripts/migrate_multi_upload.py
@@ -0,0 +1,209 @@
+"""
+数据库迁移脚本 - 添加多上传支持字段
+
+运行方式: python scripts/migrate_multi_upload.py
+
+此脚本将:
+1. 添加 upload_batch 字段到 stp_files 表
+2. 添加 volume, surface_area, product_weight 快速查询字段到 stp_files 表
+3. 移除 file_hash 字段的唯一约束(如果存在)
+4. 为新字段创建索引
+"""
+import asyncio
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from sqlalchemy import text
+from database.database import async_engine, get_db_session
+from utils.logger import get_logger
+
+logger = get_logger(__name__)
+
+
+async def check_column_exists(conn, table_name: str, column_name: str) -> bool:
+ """检查列是否存在"""
+ result = await conn.execute(text("""
+ SELECT column_name
+ FROM information_schema.columns
+ WHERE table_name = :table_name
+ AND column_name = :column_name
+ """), {"table_name": table_name, "column_name": column_name})
+ return result.fetchone() is not None
+
+
+async def check_index_exists(conn, index_name: str) -> bool:
+ """检查索引是否存在"""
+ result = await conn.execute(text("""
+ SELECT indexname
+ FROM pg_indexes
+ WHERE indexname = :index_name
+ """), {"index_name": index_name})
+ return result.fetchone() is not None
+
+
+async def check_unique_constraint_exists(conn, table_name: str, column_name: str) -> bool:
+ """检查唯一约束是否存在"""
+ result = await conn.execute(text("""
+ SELECT conname
+ FROM pg_constraint
+ WHERE conrelid = :table_name::regclass
+ AND contype = 'u'
+ AND conname LIKE :pattern
+ """), {"table_name": table_name, "pattern": f"%{column_name}%"})
+ return result.fetchone() is not None
+
+
+async def run_migration():
+ """执行迁移"""
+ logger.info("开始数据库迁移 - 多上传支持...")
+
+ async with async_engine.begin() as conn:
+ try:
+ await conn.execute(text("SELECT 1"))
+ logger.info("数据库连接成功")
+ except Exception as e:
+ logger.error(f"数据库连接失败: {e}")
+ return False
+
+ async with async_engine.begin() as conn:
+ migration_steps = []
+
+ if not await check_column_exists(conn, "stp_files", "upload_batch"):
+ migration_steps.append("添加 upload_batch 字段")
+ await conn.execute(text("""
+ ALTER TABLE stp_files
+ ADD COLUMN upload_batch VARCHAR(36)
+ """))
+ logger.info("✓ 添加 upload_batch 字段")
+
+ if not await check_column_exists(conn, "stp_files", "volume"):
+ migration_steps.append("添加 volume 字段")
+ await conn.execute(text("""
+ ALTER TABLE stp_files
+ ADD COLUMN volume FLOAT
+ """))
+ logger.info("✓ 添加 volume 字段")
+
+ if not await check_column_exists(conn, "stp_files", "surface_area"):
+ migration_steps.append("添加 surface_area 字段")
+ await conn.execute(text("""
+ ALTER TABLE stp_files
+ ADD COLUMN surface_area FLOAT
+ """))
+ logger.info("✓ 添加 surface_area 字段")
+
+ if not await check_column_exists(conn, "stp_files", "product_weight"):
+ migration_steps.append("添加 product_weight 字段")
+ await conn.execute(text("""
+ ALTER TABLE stp_files
+ ADD COLUMN product_weight FLOAT
+ """))
+ logger.info("✓ 添加 product_weight 字段")
+
+ if not await check_index_exists(conn, "ix_stp_files_upload_batch"):
+ migration_steps.append("创建 upload_batch 索引")
+ await conn.execute(text("""
+ CREATE INDEX ix_stp_files_upload_batch
+ ON stp_files(upload_batch)
+ """))
+ logger.info("✓ 创建 upload_batch 索引")
+
+ if not await check_index_exists(conn, "ix_stp_files_file_hash"):
+ migration_steps.append("创建 file_hash 索引")
+ await conn.execute(text("""
+ CREATE INDEX ix_stp_files_file_hash
+ ON stp_files(file_hash)
+ """))
+ logger.info("✓ 创建 file_hash 索引")
+
+ if not await check_index_exists(conn, "ix_stp_files_original_filename"):
+ migration_steps.append("创建 original_filename 索引")
+ await conn.execute(text("""
+ CREATE INDEX ix_stp_files_original_filename
+ ON stp_files(original_filename)
+ """))
+ logger.info("✓ 创建 original_filename 索引")
+
+ try:
+ result = await conn.execute(text("""
+ SELECT conname
+ FROM pg_constraint
+ WHERE conrelid = 'stp_files'::regclass
+ AND contype = 'u'
+ """))
+ unique_constraints = result.fetchall()
+
+ for constraint in unique_constraints:
+ constraint_name = constraint[0]
+ if 'file_hash' in constraint_name.lower():
+ migration_steps.append(f"删除唯一约束 {constraint_name}")
+ await conn.execute(text(f"""
+ ALTER TABLE stp_files
+ DROP CONSTRAINT {constraint_name}
+ """))
+ logger.info(f"✓ 删除唯一约束: {constraint_name}")
+ except Exception as e:
+ logger.warning(f"检查唯一约束时出错(可能不存在): {e}")
+
+ if migration_steps:
+ logger.info(f"\n迁移完成,执行了 {len(migration_steps)} 个步骤:")
+ for step in migration_steps:
+ logger.info(f" - {step}")
+ else:
+ logger.info("\n无需迁移,所有字段和索引已存在")
+
+ logger.info("数据库迁移完成!")
+ return True
+
+
+async def rollback_migration():
+ """回滚迁移"""
+ logger.info("开始回滚数据库迁移...")
+
+ async with async_engine.begin() as conn:
+ try:
+ if await check_index_exists(conn, "ix_stp_files_upload_batch"):
+ await conn.execute(text("DROP INDEX IF EXISTS ix_stp_files_upload_batch"))
+ logger.info("✓ 删除 upload_batch 索引")
+
+ if await check_index_exists(conn, "ix_stp_files_file_hash"):
+ await conn.execute(text("DROP INDEX IF EXISTS ix_stp_files_file_hash"))
+ logger.info("✓ 删除 file_hash 索引")
+
+ if await check_column_exists(conn, "stp_files", "upload_batch"):
+ await conn.execute(text("ALTER TABLE stp_files DROP COLUMN upload_batch"))
+ logger.info("✓ 删除 upload_batch 字段")
+
+ if await check_column_exists(conn, "stp_files", "volume"):
+ await conn.execute(text("ALTER TABLE stp_files DROP COLUMN volume"))
+ logger.info("✓ 删除 volume 字段")
+
+ if await check_column_exists(conn, "stp_files", "surface_area"):
+ await conn.execute(text("ALTER TABLE stp_files DROP COLUMN surface_area"))
+ logger.info("✓ 删除 surface_area 字段")
+
+ if await check_column_exists(conn, "stp_files", "product_weight"):
+ await conn.execute(text("ALTER TABLE stp_files DROP COLUMN product_weight"))
+ logger.info("✓ 删除 product_weight 字段")
+
+ logger.info("回滚完成!")
+ except Exception as e:
+ logger.error(f"回滚失败: {e}")
+ return False
+
+ return True
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser(description="数据库迁移脚本 - 多上传支持")
+ parser.add_argument("--rollback", action="store_true", help="回滚迁移")
+ args = parser.parse_args()
+
+ if args.rollback:
+ asyncio.run(rollback_migration())
+ else:
+ asyncio.run(run_migration())
diff --git a/scripts/verify_stp.py b/scripts/verify_stp.py
new file mode 100644
index 0000000..9c802ee
--- /dev/null
+++ b/scripts/verify_stp.py
@@ -0,0 +1,439 @@
+#!/usr/bin/env python3
+"""
+STP文件几何验证脚本
+使用FreeCAD命令行验证PythonOCC提取的几何数据是否正确
+
+使用方法:
+ freecad -c verify_stp.py
+
+或直接运行:
+ python verify_stp.py
+"""
+
+import sys
+import os
+import json
+from datetime import datetime
+from pathlib import Path
+
+# 添加项目路径
+PROJECT_ROOT = Path(__file__).parent.parent
+sys.path.insert(0, str(PROJECT_ROOT / "src"))
+
+# 全局结果字典
+verification_result = {
+ "file": "",
+ "timestamp": "",
+ "freecad": {},
+ "pythonocc": {},
+ "comparison": {},
+ "status": "pending"
+}
+
+
+def verify_with_freecad(stp_path):
+ """使用FreeCAD验证STP文件"""
+ try:
+ import FreeCAD
+ import Part
+ import Mesh
+ import MeshPart
+
+ print(f"\n{'='*70}")
+ print(f"FreeCAD 验证")
+ print(f"{'='*70}\n")
+
+ # 创建新文档
+ doc = FreeCAD.newDocument("Verification")
+
+ # 导入STP文件
+ Part.insert(stp_path, "Verification")
+
+ # 获取所有形状对象
+ shapes = []
+ for obj in doc.Objects:
+ if hasattr(obj, 'Shape') and not obj.Shape.isNull():
+ shapes.append(obj.Shape)
+
+ if not shapes:
+ print("❌ 未找到有效的几何形状")
+ return None
+
+ # 合并所有形状
+ compound = shapes[0]
+ for s in shapes[1:]:
+ compound = compound.fuse(s)
+
+ # 提取几何数据
+ bbox = compound.BoundBox
+
+ # 计算体积和表面积
+ volume_mm3 = compound.Volume
+ surface_area_mm2 = compound.Area
+
+ # 获取质心
+ com = compound.CenterOfMass
+
+ # 拓扑信息
+ topology = {
+ "faces": len(compound.Faces),
+ "edges": len(compound.Edges),
+ "vertices": len(compound.Vertexes),
+ "wires": len(compound.Wires),
+ "shells": len(compound.Shells),
+ "solids": len(compound.Solids)
+ }
+
+ # 生成网格用于点云验证
+ mesh = MeshPart.meshFromShape(compound, LinearDeflection=0.1, AngularDeflection=0.5)
+
+ result = {
+ "volume_mm3": float(volume_mm3),
+ "volume_cm3": float(volume_mm3 / 1000),
+ "surface_area_mm2": float(surface_area_mm2),
+ "surface_area_cm2": float(surface_area_mm2 / 100),
+ "bounding_box": {
+ "x_min": float(bbox.XMin),
+ "x_max": float(bbox.XMax),
+ "y_min": float(bbox.YMin),
+ "y_max": float(bbox.YMax),
+ "z_min": float(bbox.ZMin),
+ "z_max": float(bbox.ZMax),
+ "x_length": float(bbox.XLength),
+ "y_length": float(bbox.YLength),
+ "z_length": float(bbox.ZLength),
+ "center": [float(bbox.Center.x), float(bbox.Center.y), float(bbox.Center.z)]
+ },
+ "center_of_mass": [float(com.x), float(com.y), float(com.z)],
+ "topology": topology,
+ "mesh": {
+ "points": mesh.CountPoints,
+ "facets": mesh.CountFacets
+ }
+ }
+
+ # 打印结果
+ print(f"体积: {result['volume_cm3']:.4f} cm³ ({result['volume_mm3']:.2f} mm³)")
+ print(f"表面积: {result['surface_area_cm2']:.4f} cm² ({result['surface_area_mm2']:.2f} mm²)")
+ print(f"\n边界框:")
+ print(f" X: {result['bounding_box']['x_length']:.4f} mm ({result['bounding_box']['x_min']:.2f} ~ {result['bounding_box']['x_max']:.2f})")
+ print(f" Y: {result['bounding_box']['y_length']:.4f} mm ({result['bounding_box']['y_min']:.2f} ~ {result['bounding_box']['y_max']:.2f})")
+ print(f" Z: {result['bounding_box']['z_length']:.4f} mm ({result['bounding_box']['z_min']:.2f} ~ {result['bounding_box']['z_max']:.2f})")
+ print(f" 中心: ({result['bounding_box']['center'][0]:.2f}, {result['bounding_box']['center'][1]:.2f}, {result['bounding_box']['center'][2]:.2f})")
+ print(f"\n质心: ({result['center_of_mass'][0]:.4f}, {result['center_of_mass'][1]:.4f}, {result['center_of_mass'][2]:.4f})")
+ print(f"\n拓扑信息:")
+ print(f" 面: {topology['faces']}")
+ print(f" 边: {topology['edges']}")
+ print(f" 顶点: {topology['vertices']}")
+ print(f" 线框: {topology['wires']}")
+ print(f" 壳体: {topology['shells']}")
+ print(f" 实体: {topology['solids']}")
+ print(f"\n网格:")
+ print(f" 点数: {result['mesh']['points']}")
+ print(f" 面数: {result['mesh']['facets']}")
+
+ # 关闭文档
+ FreeCAD.closeDocument("Verification")
+
+ return result
+
+ except Exception as e:
+ print(f"❌ FreeCAD验证失败: {e}")
+ import traceback
+ traceback.print_exc()
+ return None
+
+
+def verify_with_pythonocc(stp_path):
+ """使用PythonOCC验证STP文件"""
+ try:
+ from OCC.Core.STEPControl import STEPControl_Reader
+ from OCC.Core.IFSelect import IFSelect_RetDone
+ from OCC.Core.BRepTools import breptools_Read
+ from OCC.Core.TopExp import TopExp_Explorer
+ from OCC.Core.TopAbs import TopAbs_SOLID, TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
+ from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties
+ from OCC.Core.GProp import GProp_GProps
+ from OCC.Core.Bnd import Bnd_Box
+ from OCC.Core.BRepBndLib import brepbndlib_Add
+ from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
+ from OCC.Core.TopLoc import TopLoc_Location
+ from OCC.Core.BRep import BRep_Tool
+
+ print(f"\n{'='*70}")
+ print(f"PythonOCC 验证")
+ print(f"{'='*70}\n")
+
+ # 读取STP文件
+ reader = STEPControl_Reader()
+ status = reader.ReadFile(stp_path)
+
+ if status != IFSelect_RetDone:
+ print("❌ 无法读取STP文件")
+ return None
+
+ reader.TransferRoots()
+ shape = reader.OneShape()
+
+ # 生成网格
+ mesh_gen = BRepMesh_IncrementalMesh(shape, 0.1)
+ mesh_gen.Perform()
+
+ # 计算体积
+ vol_props = GProp_GProps()
+ brepgprop_VolumeProperties(shape, vol_props)
+ volume_mm3 = vol_props.Mass()
+ com = vol_props.CentreOfMass()
+
+ # 计算表面积
+ surf_props = GProp_GProps()
+ brepgprop_SurfaceProperties(shape, surf_props)
+ surface_area_mm2 = surf_props.Mass()
+
+ # 计算边界框
+ bbox = Bnd_Box()
+ brepbndlib_Add(shape, bbox)
+ xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
+
+ # 拓扑统计
+ def count_topology(shape, top_type):
+ explorer = TopExp_Explorer(shape, top_type)
+ count = 0
+ while explorer.More():
+ count += 1
+ explorer.Next()
+ return count
+
+ topology = {
+ "faces": count_topology(shape, TopAbs_FACE),
+ "edges": count_topology(shape, TopAbs_EDGE),
+ "vertices": count_topology(shape, TopAbs_VERTEX),
+ "solids": count_topology(shape, TopAbs_SOLID)
+ }
+
+ # 统计网格顶点和三角形
+ mesh_points = 0
+ mesh_facets = 0
+ explorer = TopExp_Explorer(shape, TopAbs_FACE)
+ while explorer.More():
+ face = explorer.Current()
+ location = TopLoc_Location()
+ triangulation = BRep_Tool.Triangulation(face, location)
+ if triangulation:
+ mesh_points += triangulation.NbNodes()
+ mesh_facets += triangulation.NbTriangles()
+ explorer.Next()
+
+ result = {
+ "volume_mm3": float(volume_mm3),
+ "volume_cm3": float(volume_mm3 / 1000),
+ "surface_area_mm2": float(surface_area_mm2),
+ "surface_area_cm2": float(surface_area_mm2 / 100),
+ "bounding_box": {
+ "x_min": float(xmin),
+ "x_max": float(xmax),
+ "y_min": float(ymin),
+ "y_max": float(ymax),
+ "z_min": float(zmin),
+ "z_max": float(zmax),
+ "x_length": float(xmax - xmin),
+ "y_length": float(ymax - ymin),
+ "z_length": float(zmax - zmin),
+ "center": [float((xmin + xmax) / 2), float((ymin + ymax) / 2), float((zmin + zmax) / 2)]
+ },
+ "center_of_mass": [float(com.X()), float(com.Y()), float(com.Z())],
+ "topology": topology,
+ "mesh": {
+ "points": mesh_points,
+ "facets": mesh_facets
+ }
+ }
+
+ # 打印结果
+ print(f"体积: {result['volume_cm3']:.4f} cm³ ({result['volume_mm3']:.2f} mm³)")
+ print(f"表面积: {result['surface_area_cm2']:.4f} cm² ({result['surface_area_mm2']:.2f} mm²)")
+ print(f"\n边界框:")
+ print(f" X: {result['bounding_box']['x_length']:.4f} mm ({result['bounding_box']['x_min']:.2f} ~ {result['bounding_box']['x_max']:.2f})")
+ print(f" Y: {result['bounding_box']['y_length']:.4f} mm ({result['bounding_box']['y_min']:.2f} ~ {result['bounding_box']['y_max']:.2f})")
+ print(f" Z: {result['bounding_box']['z_length']:.4f} mm ({result['bounding_box']['z_min']:.2f} ~ {result['bounding_box']['z_max']:.2f})")
+ print(f" 中心: ({result['bounding_box']['center'][0]:.2f}, {result['bounding_box']['center'][1]:.2f}, {result['bounding_box']['center'][2]:.2f})")
+ print(f"\n质心: ({result['center_of_mass'][0]:.4f}, {result['center_of_mass'][1]:.4f}, {result['center_of_mass'][2]:.4f})")
+ print(f"\n拓扑信息:")
+ print(f" 面: {topology['faces']}")
+ print(f" 边: {topology['edges']}")
+ print(f" 顶点: {topology['vertices']}")
+ print(f" 实体: {topology['solids']}")
+ print(f"\n网格:")
+ print(f" 点数: {result['mesh']['points']}")
+ print(f" 面数: {result['mesh']['facets']}")
+
+ return result
+
+ except Exception as e:
+ print(f"❌ PythonOCC验证失败: {e}")
+ import traceback
+ traceback.print_exc()
+ return None
+
+
+def compare_results(freecad_result, pythonocc_result):
+ """对比FreeCAD和PythonOCC的结果"""
+ print(f"\n{'='*70}")
+ print(f"对比结果")
+ print(f"{'='*70}\n")
+
+ if not freecad_result or not pythonocc_result:
+ print("❌ 无法对比,缺少数据")
+ return None
+
+ comparison = {}
+
+ # 体积对比
+ vol_diff = abs(freecad_result['volume_mm3'] - pythonocc_result['volume_mm3'])
+ vol_diff_percent = (vol_diff / freecad_result['volume_mm3']) * 100 if freecad_result['volume_mm3'] > 0 else 0
+
+ comparison['volume'] = {
+ "freecad": freecad_result['volume_cm3'],
+ "pythonocc": pythonocc_result['volume_cm3'],
+ "difference_mm3": vol_diff,
+ "difference_percent": vol_diff_percent
+ }
+
+ print(f"体积对比:")
+ print(f" FreeCAD: {freecad_result['volume_cm3']:.4f} cm³")
+ print(f" PythonOCC: {pythonocc_result['volume_cm3']:.4f} cm³")
+ print(f" 差异: {vol_diff:.4f} mm³ ({vol_diff_percent:.4f}%)")
+ print(f" 状态: {'✅ 通过' if vol_diff_percent < 1 else '❌ 超差'}")
+
+ # 表面积对比
+ area_diff = abs(freecad_result['surface_area_mm2'] - pythonocc_result['surface_area_mm2'])
+ area_diff_percent = (area_diff / freecad_result['surface_area_mm2']) * 100 if freecad_result['surface_area_mm2'] > 0 else 0
+
+ comparison['surface_area'] = {
+ "freecad": freecad_result['surface_area_cm2'],
+ "pythonocc": pythonocc_result['surface_area_cm2'],
+ "difference_mm2": area_diff,
+ "difference_percent": area_diff_percent
+ }
+
+ print(f"\n表面积对比:")
+ print(f" FreeCAD: {freecad_result['surface_area_cm2']:.4f} cm²")
+ print(f" PythonOCC: {pythonocc_result['surface_area_cm2']:.4f} cm²")
+ print(f" 差异: {area_diff:.4f} mm² ({area_diff_percent:.4f}%)")
+ print(f" 状态: {'✅ 通过' if area_diff_percent < 2 else '❌ 超差'}")
+
+ # 边界框对比
+ bbox_fc = freecad_result['bounding_box']
+ bbox_occ = pythonocc_result['bounding_box']
+
+ x_diff = abs(bbox_fc['x_length'] - bbox_occ['x_length'])
+ y_diff = abs(bbox_fc['y_length'] - bbox_occ['y_length'])
+ z_diff = abs(bbox_fc['z_length'] - bbox_occ['z_length'])
+
+ comparison['bounding_box'] = {
+ "x": {"freecad": bbox_fc['x_length'], "pythonocc": bbox_occ['x_length'], "difference": x_diff},
+ "y": {"freecad": bbox_fc['y_length'], "pythonocc": bbox_occ['y_length'], "difference": y_diff},
+ "z": {"freecad": bbox_fc['z_length'], "pythonocc": bbox_occ['z_length'], "difference": z_diff}
+ }
+
+ print(f"\n边界框对比:")
+ print(f" X轴: FreeCAD={bbox_fc['x_length']:.4f}, PythonOCC={bbox_occ['x_length']:.4f}, 差异={x_diff:.4f} mm")
+ print(f" Y轴: FreeCAD={bbox_fc['y_length']:.4f}, PythonOCC={bbox_occ['y_length']:.4f}, 差异={y_diff:.4f} mm")
+ print(f" Z轴: FreeCAD={bbox_fc['z_length']:.4f}, PythonOCC={bbox_occ['z_length']:.4f}, 差异={z_diff:.4f} mm")
+
+ # 质心对比
+ com_fc = freecad_result['center_of_mass']
+ com_occ = pythonocc_result['center_of_mass']
+ com_diff = [
+ abs(com_fc[0] - com_occ[0]),
+ abs(com_fc[1] - com_occ[1]),
+ abs(com_fc[2] - com_occ[2])
+ ]
+
+ comparison['center_of_mass'] = {
+ "freecad": com_fc,
+ "pythonocc": com_occ,
+ "difference": com_diff
+ }
+
+ print(f"\n质心对比:")
+ print(f" FreeCAD: ({com_fc[0]:.4f}, {com_fc[1]:.4f}, {com_fc[2]:.4f})")
+ print(f" PythonOCC: ({com_occ[0]:.4f}, {com_occ[1]:.4f}, {com_occ[2]:.4f})")
+ print(f" 差异: ({com_diff[0]:.4f}, {com_diff[1]:.4f}, {com_diff[2]:.4f}) mm")
+
+ # 拓扑对比
+ topo_fc = freecad_result['topology']
+ topo_occ = pythonocc_result['topology']
+
+ comparison['topology'] = {
+ "faces": {"freecad": topo_fc['faces'], "pythonocc": topo_occ['faces']},
+ "edges": {"freecad": topo_fc['edges'], "pythonocc": topo_occ['edges']},
+ "vertices": {"freecad": topo_fc['vertices'], "pythonocc": topo_occ['vertices']}
+ }
+
+ print(f"\n拓扑对比:")
+ print(f" 面: FreeCAD={topo_fc['faces']}, PythonOCC={topo_occ['faces']}")
+ print(f" 边: FreeCAD={topo_fc['edges']}, PythonOCC={topo_occ['edges']}")
+ print(f" 顶点: FreeCAD={topo_fc['vertices']}, PythonOCC={topo_occ['vertices']}")
+
+ # 总体评估
+ passed = vol_diff_percent < 1 and area_diff_percent < 2
+
+ print(f"\n{'='*70}")
+ print(f"验证结果: {'✅ 通过' if passed else '❌ 失败'}")
+ print(f"{'='*70}\n")
+
+ comparison['overall_status'] = "passed" if passed else "failed"
+
+ return comparison
+
+
+def main():
+ """主函数"""
+ if len(sys.argv) < 2:
+ print("用法: python verify_stp.py ")
+ print(" 或: freecad -c verify_stp.py ")
+ sys.exit(1)
+
+ stp_path = sys.argv[1]
+
+ if not os.path.exists(stp_path):
+ print(f"❌ 文件不存在: {stp_path}")
+ sys.exit(1)
+
+ # 初始化结果
+ verification_result["file"] = stp_path
+ verification_result["timestamp"] = datetime.now().isoformat()
+
+ print(f"\n{'#'*70}")
+ print(f"# STP文件几何验证报告")
+ print(f"# 文件: {stp_path}")
+ print(f"# 时间: {verification_result['timestamp']}")
+ print(f"{'#'*70}")
+
+ # FreeCAD验证
+ freecad_result = verify_with_freecad(stp_path)
+ verification_result["freecad"] = freecad_result or {}
+
+ # PythonOCC验证
+ pythonocc_result = verify_with_pythonocc(stp_path)
+ verification_result["pythonocc"] = pythonocc_result or {}
+
+ # 对比结果
+ comparison = compare_results(freecad_result, pythonocc_result)
+ verification_result["comparison"] = comparison or {}
+ verification_result["status"] = comparison.get('overall_status', 'failed') if comparison else 'failed'
+
+ # 保存JSON报告
+ report_path = Path(stp_path).stem + "_verification_report.json"
+ with open(report_path, 'w', encoding='utf-8') as f:
+ json.dump(verification_result, f, indent=2, ensure_ascii=False)
+
+ print(f"\n验证报告已保存: {report_path}")
+
+ return verification_result
+
+
+if __name__ == "__main__":
+ result = main()
+ sys.exit(0 if result['status'] == 'passed' else 1)
diff --git a/src/api/routes.py b/src/api/routes.py
index 50f36fb..13a4ae3 100644
--- a/src/api/routes.py
+++ b/src/api/routes.py
@@ -218,111 +218,33 @@ async def debug_tasks():
@router.get("/history")
@router.post("/history")
async def get_file_history(db_session: AsyncSession = Depends(get_db_session)):
- """获取按文件名分组的文件历史记录"""
- from sqlalchemy import select
- from models.database import ProcessingTask, STPFile
+ """获取按文件名分组的文件历史记录(支持多上传)"""
+ storage_service = StorageIntegrationService()
- # 从数据库查询所有处理任务
- result = await db_session.execute(
- select(ProcessingTask, STPFile)
- .join(STPFile, ProcessingTask.stp_file_id == STPFile.id)
- .order_by(ProcessingTask.created_time.desc())
- )
-
- tasks = result.all()
-
- # 按文件名分组
- file_groups = {}
- for task, stp_file in tasks:
- filename = stp_file.original_filename
- if filename not in file_groups:
- file_groups[filename] = []
-
- file_groups[filename].append({
- "task_id": task.task_id,
- "filename": filename,
- "upload_time": task.created_time.isoformat() if task.created_time else "",
- "status": task.status,
- "file_size": stp_file.file_size
- })
-
- # 构建返回数据
- files = []
- for filename, file_tasks in file_groups.items():
- # 按上传时间排序
- file_tasks.sort(key=lambda x: x.get("upload_time", ""), reverse=True)
-
- files.append({
- "filename": filename,
- "record_count": len(file_tasks),
- "last_upload": file_tasks[0].get("upload_time", ""),
- "first_upload": file_tasks[-1].get("upload_time", "") if len(file_tasks) > 1 else ""
- })
-
- # 按最后上传时间排序
- files.sort(key=lambda x: x["last_upload"], reverse=True)
+ file_groups = await storage_service.get_all_file_groups(db_session)
return {
- "total_files": len(files),
- "files": files
+ "total_files": len(file_groups),
+ "files": file_groups
}
@router.get("/history/{filename}")
@router.post("/history/{filename}")
async def get_file_records(filename: str, db_session: AsyncSession = Depends(get_db_session)):
- """获取指定文件名的所有记录"""
- # URL解码文件名
+ """获取指定文件名的所有上传记录(支持多上传历史)"""
import urllib.parse
decoded_filename = urllib.parse.unquote(filename)
- # 从数据库查询指定文件名的所有处理任务
- from sqlalchemy import select
- from models.database import ProcessingTask, STPFile
-
- result = await db_session.execute(
- select(ProcessingTask, STPFile)
- .join(STPFile, ProcessingTask.stp_file_id == STPFile.id)
- .where(STPFile.original_filename == decoded_filename)
- .order_by(ProcessingTask.created_time.desc())
+ storage_service = StorageIntegrationService()
+ file_records = await storage_service.get_file_history_by_filename(
+ db_session,
+ decoded_filename
)
- tasks = result.all()
-
- # 构建返回数据
- file_records = []
- for task, stp_file in tasks:
- file_records.append({
- "task_id": task.task_id,
- "filename": stp_file.original_filename,
- "file_size": stp_file.file_size,
- "upload_time": task.created_time.isoformat() if task.created_time else "",
- "status": task.status,
- "completed_at": task.completed_time.isoformat() if task.completed_time else ""
- })
-
- # 按上传时间排序(最新的在前)
- file_records.sort(key=lambda x: x.get("upload_time", ""), reverse=True)
-
return file_records
-@router.get("/history")
-@router.post("/history")
-async def history_page(request: Request):
- """历史记录页面"""
- from fastapi.templating import Jinja2Templates
- import os
- # 简化路径配置,直接使用当前工作目录下的templates文件夹
- templates_dir = os.path.join(os.getcwd(), "templates")
- templates = Jinja2Templates(directory=templates_dir)
- return templates.TemplateResponse("history.html", {
- "request": request,
- "pythonocc_available": True,
- "version": "3.0.0"
- })
-
-
@router.get("/result/{task_id}")
@router.post("/result/{task_id}")
async def result_page(request: Request, task_id: str, db_session: AsyncSession = Depends(get_db_session)):
@@ -698,6 +620,15 @@ async def process_file_core(
# 保存质量指标和分析摘要到数据库
await _save_analysis_metrics(db_session, stp_file_id, analysis_result)
+ # 9.6 更新STP文件的分析摘要字段(用于快速查询)
+ await storage_service.update_stp_file_analysis_summary(
+ db_session,
+ stp_file_id,
+ volume=volume_mm3,
+ surface_area=surface_area_mm2,
+ product_weight=product_weight_g
+ )
+
# 10. 完成处理
await storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
await storage_service.update_task_status(
diff --git a/src/models/database.py b/src/models/database.py
index e9a07c9..000a065 100644
--- a/src/models/database.py
+++ b/src/models/database.py
@@ -115,7 +115,7 @@ class RolePermission(Base):
return f""
class STPFile(Base):
- """STP源文件元数据表"""
+ """STP源文件元数据表 - 支持同一文件多次上传"""
__tablename__ = "stp_files"
id = Column(Integer, primary_key=True, index=True)
@@ -127,19 +127,27 @@ class STPFile(Base):
object_url = Column(String(1000), nullable=True) # 预签名URL(可选)
# 文件信息
- original_filename = Column(String(255), nullable=False)
+ original_filename = Column(String(255), nullable=False, index=True) # 添加索引支持按文件名查询
file_size = Column(Integer, nullable=False)
- file_hash = Column(String(64), unique=True, index=True) # SHA256 hash
+ file_hash = Column(String(64), index=True) # 移除unique约束,允许同一文件多次上传
mime_type = Column(String(50), default="application/octet-stream")
+ # 上传批次标识 - 用于区分同一文件的多次上传
+ upload_batch = Column(String(36), index=True) # UUID批次号
+
# 时间戳
upload_time = Column(DateTime, default=func.now())
processed_time = Column(DateTime, nullable=True)
# 状态
- status = Column(String(20), default="pending") # pending, processing, completed, failed
+ status = Column(String(20), default="pending", index=True) # pending, processing, completed, failed
error_message = Column(Text, nullable=True)
+ # 分析摘要 - 快速查询字段
+ volume = Column(Float, nullable=True) # 体积 mm³
+ surface_area = Column(Float, nullable=True) # 表面积 mm²
+ product_weight = Column(Float, nullable=True) # 产品重量 g
+
# 保留旧字段以兼容
file_path = Column(String(500), nullable=True) # 本地路径(已弃用)
file_content = Column(LargeBinary, nullable=True) # 本地存储(已弃用)
@@ -148,14 +156,15 @@ class STPFile(Base):
# 关联关系
user = relationship("User", back_populates="stp_files")
geometry_data = relationship("GeometryData", back_populates="stp_file", uselist=False)
- # 网格数据:一条 STP 记录对应一条网格摘要记录(详细 JSON 在 RustFS)
mesh_data = relationship("MeshData", back_populates="stp_file", uselist=False)
mold_cavity_data = relationship("MoldCavityData", back_populates="stp_file", uselist=False)
html_file = relationship("HTMLFile", back_populates="stp_file", uselist=False)
analysis_metrics = relationship("AnalysisMetrics", back_populates="stp_file", uselist=False)
+ feature_detections = relationship("FeatureDetection", back_populates="stp_file")
+ design_recommendations = relationship("DesignRecommendation", back_populates="stp_file")
def __repr__(self):
- return f""
+ return f""
class GeometryData(Base):
"""几何数据JSON元数据表"""
@@ -360,6 +369,9 @@ class FeatureDetection(Base):
# 关联的几何数据
geometry_data_id = Column(Integer, ForeignKey("geometry_data.id"), nullable=True)
+ # 关联关系
+ stp_file = relationship("STPFile", back_populates="feature_detections")
+
def __repr__(self):
return f""
@@ -388,6 +400,9 @@ class DesignRecommendation(Base):
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, nullable=True)
+ # 关联关系
+ stp_file = relationship("STPFile", back_populates="design_recommendations")
+
def __repr__(self):
return f""
diff --git a/src/services/storage_integration_rustfs.py b/src/services/storage_integration_rustfs.py
index 4095bbb..024a191 100644
--- a/src/services/storage_integration_rustfs.py
+++ b/src/services/storage_integration_rustfs.py
@@ -6,6 +6,7 @@ from pathlib import Path
from typing import Optional, Dict, Any
import json
from datetime import datetime
+import uuid
from models.database import (
STPFile, GeometryData, MeshData, MoldCavityData,
@@ -25,8 +26,12 @@ class StorageIntegrationService:
async def save_stp_file(self, session: AsyncSession,
file_path: Path,
original_filename: str,
- user_id: Optional[int] = None) -> STPFile:
- """保存STP文件到PostgreSQL元数据 + RustFS对象存储"""
+ user_id: Optional[int] = None,
+ upload_batch: Optional[str] = None) -> STPFile:
+ """保存STP文件到PostgreSQL元数据 + RustFS对象存储
+
+ 支持同一文件多次上传,每次上传都会创建新记录
+ """
# 1. 上传到RustFS
upload_result = await rustfs_manager.upload_file(
@@ -35,23 +40,15 @@ class StorageIntegrationService:
original_filename=original_filename,
metadata={
'original_filename': original_filename,
- 'user_id': str(user_id) if user_id else 'anonymous'
+ 'user_id': str(user_id) if user_id else 'anonymous',
+ 'upload_batch': upload_batch or str(uuid.uuid4())
}
)
file_hash = upload_result['file_hash']
-
- # 2. 检查是否已存在相同文件
- existing_file = await session.execute(
- select(STPFile).where(STPFile.file_hash == file_hash)
- )
- existing_file = existing_file.scalar_one_or_none()
-
- if existing_file:
- logger.info(f"文件已存在,返回现有记录: {existing_file.id}")
- return existing_file
+ batch_id = upload_batch or str(uuid.uuid4())
- # 3. 创建新PostgreSQL记录
+ # 2. 创建新PostgreSQL记录(每次上传都创建新记录)
stp_file = STPFile(
user_id=user_id,
object_key=upload_result['object_key'],
@@ -59,15 +56,16 @@ class StorageIntegrationService:
original_filename=original_filename,
file_size=upload_result['file_size'],
file_hash=file_hash,
+ upload_batch=batch_id,
status="uploaded",
- file_path=str(file_path) # 保留本地路径以兼容
+ file_path=str(file_path)
)
session.add(stp_file)
await session.commit()
await session.refresh(stp_file)
- logger.info(f"STP文件保存成功 RustFS: {stp_file.id}")
+ logger.info(f"STP文件保存成功 RustFS: {stp_file.id}, 批次: {batch_id}")
return stp_file
async def create_processing_task(self, session: AsyncSession,
@@ -415,7 +413,7 @@ class StorageIntegrationService:
resource_type=resource_type,
resource_id=resource_id,
description=description,
- metadata=metadata,
+ meta_data=metadata,
ip_address=ip_address,
user_agent=user_agent
)
@@ -529,6 +527,140 @@ class StorageIntegrationService:
return result
+ async def get_file_history_by_filename(
+ self,
+ session: AsyncSession,
+ filename: str,
+ user_id: Optional[int] = None,
+ limit: int = 50
+ ) -> list:
+ """获取同一文件名的所有上传历史记录"""
+
+ query = select(STPFile).where(
+ STPFile.original_filename == filename
+ ).order_by(STPFile.upload_time.desc())
+
+ if user_id:
+ query = query.where(STPFile.user_id == user_id)
+
+ query = query.limit(limit)
+
+ result = await session.execute(query)
+ files = result.scalars().all()
+
+ return [
+ {
+ 'id': f.id,
+ 'upload_batch': f.upload_batch,
+ 'upload_time': f.upload_time.isoformat() if f.upload_time else None,
+ 'file_size': f.file_size,
+ 'status': f.status,
+ 'volume': f.volume,
+ 'surface_area': f.surface_area,
+ 'product_weight': f.product_weight,
+ 'has_analysis': f.status == 'completed'
+ }
+ for f in files
+ ]
+
+ async def get_all_file_groups(
+ self,
+ session: AsyncSession,
+ user_id: Optional[int] = None,
+ limit: int = 100
+ ) -> list:
+ """获取所有文件分组(按文件名分组),包含每个文件的最新分析结果"""
+
+ from sqlalchemy import func, desc
+
+ # 子查询:获取每个文件名的最新上传
+ subquery = (
+ select(
+ STPFile.original_filename,
+ func.max(STPFile.upload_time).label('latest_upload')
+ )
+ .group_by(STPFile.original_filename)
+ .order_by(desc('latest_upload'))
+ .limit(limit)
+ )
+
+ if user_id:
+ subquery = subquery.where(STPFile.user_id == user_id)
+
+ subquery = subquery.subquery()
+
+ # 主查询:获取最新记录和统计信息
+ query = (
+ select(STPFile)
+ .join(
+ subquery,
+ (STPFile.original_filename == subquery.c.original_filename) &
+ (STPFile.upload_time == subquery.c.latest_upload)
+ )
+ .order_by(STPFile.upload_time.desc())
+ )
+
+ result = await session.execute(query)
+ latest_files = result.scalars().all()
+
+ # 获取每个文件名的上传次数
+ file_groups = []
+ for f in latest_files:
+ count_query = select(func.count()).where(
+ STPFile.original_filename == f.original_filename
+ )
+ if user_id:
+ count_query = count_query.where(STPFile.user_id == user_id)
+
+ count_result = await session.execute(count_query)
+ upload_count = count_result.scalar()
+
+ file_groups.append({
+ 'filename': f.original_filename,
+ 'latest_id': f.id,
+ 'latest_upload_time': f.upload_time.isoformat() if f.upload_time else None,
+ 'latest_status': f.status,
+ 'upload_count': upload_count,
+ 'file_size': f.file_size,
+ 'volume': f.volume,
+ 'surface_area': f.surface_area,
+ 'product_weight': f.product_weight
+ })
+
+ return file_groups
+
+ async def update_stp_file_analysis_summary(
+ self,
+ session: AsyncSession,
+ stp_file_id: int,
+ volume: Optional[float] = None,
+ surface_area: Optional[float] = None,
+ product_weight: Optional[float] = None
+ ):
+ """更新STP文件的分析摘要字段(用于快速查询)"""
+ try:
+ update_data = {}
+ if volume is not None:
+ update_data['volume'] = volume
+ if surface_area is not None:
+ update_data['surface_area'] = surface_area
+ if product_weight is not None:
+ update_data['product_weight'] = product_weight
+
+ if update_data:
+ await session.execute(
+ update(STPFile)
+ .where(STPFile.id == stp_file_id)
+ .values(**update_data)
+ )
+ await session.commit()
+ logger.info(f"STP文件分析摘要更新: ID {stp_file_id}")
+
+ except Exception as e:
+ await session.rollback()
+ logger.error(f"更新STP文件分析摘要失败: {e}")
+ raise
+
async def delete_stp_file_cascade(self, session: AsyncSession,
stp_file_id: int):
"""级联删除STP文件及其所有关联数据"""
diff --git a/static/vue-app.js b/static/vue-app.js
index bdb5a12..93d64d6 100644
--- a/static/vue-app.js
+++ b/static/vue-app.js
@@ -760,7 +760,10 @@ const MoldInsightView = {
polling: false,
dragOver: false,
progress: 0,
- history: null
+ history: null,
+ showFileHistory: false,
+ selectedFileHistory: null,
+ fileHistoryRecords: []
});
const loadHistory = async () => {
@@ -771,6 +774,21 @@ const MoldInsightView = {
}
};
+ const viewFileHistory = async (filename) => {
+ try {
+ state.selectedFileHistory = filename;
+ state.fileHistoryRecords = await apiRequest(`/api/history/${encodeURIComponent(filename)}`);
+ state.showFileHistory = true;
+ } catch (e) {
+ handleApiError(e, '加载文件历史');
+ }
+ };
+
+ const viewResult = (record) => {
+ state.showFileHistory = false;
+ router.push(`/moldinsight/result/${record.id}`);
+ };
+
const handleFileChange = (event) => {
const file = event.target.files[0];
if (!file) return;
@@ -844,6 +862,7 @@ const MoldInsightView = {
state.polling = false;
state.progress = 100;
addNotification('分析完成', 'success');
+ loadHistory();
router.push(`/moldinsight/result/${taskId}`);
return;
}
@@ -879,7 +898,9 @@ const MoldInsightView = {
handleDrop,
uploadFile,
formatFileSize,
- formatDateTime
+ formatDateTime,
+ viewFileHistory,
+ viewResult
};
},
template: `
@@ -933,38 +954,90 @@ const MoldInsightView = {
-
最近分析
+
分析历史
| 文件名 |
- 大小 |
- 状态 |
- 分析时间 |
+ 上传次数 |
+ 文件大小 |
+ 最新状态 |
+ 最新分析时间 |
操作 |
-
+
| {{ file.filename }} |
+
+ {{ file.upload_count }} 次
+ |
{{ formatFileSize(file.file_size) }} |
-
- {{ file.status }}
+
+ {{ file.latest_status }}
|
- {{ formatDateTime(file.created_at) }} |
+ {{ formatDateTime(file.latest_upload_time) }} |
-
+
+
+
+
|
+
+
+
+
+
+
+
+
+ | 上传时间 |
+ 文件大小 |
+ 状态 |
+ 体积 (mm³) |
+ 表面积 (mm²) |
+ 重量 (g) |
+ 操作 |
+
+
+
+
+ | {{ formatDateTime(record.upload_time) }} |
+ {{ formatFileSize(record.file_size) }} |
+
+
+ {{ record.status }}
+
+ |
+ {{ record.volume ? formatNumber(record.volume) : '-' }} |
+ {{ record.surface_area ? formatNumber(record.surface_area) : '-' }} |
+ {{ record.product_weight ? record.product_weight.toFixed(2) : '-' }} |
+
+
+ |
+
+
+
+
+
+
`
};