init
This commit is contained in:
+107
@@ -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界面
|
||||
+104
@@ -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修复,向后兼容
|
||||
@@ -1,77 +1,294 @@
|
||||
# MoldInsight - 模具几何分析系统
|
||||
# Gemold - 模具制造管理系统
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 项目简介
|
||||
|
||||
**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}
|
||||
### 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)
|
||||
|
||||
+72
-31
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -0,0 +1,439 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
STP文件几何验证脚本
|
||||
使用FreeCAD命令行验证PythonOCC提取的几何数据是否正确
|
||||
|
||||
使用方法:
|
||||
freecad -c verify_stp.py <stp_file_path>
|
||||
|
||||
或直接运行:
|
||||
python verify_stp.py <stp_file_path>
|
||||
"""
|
||||
|
||||
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 <stp_file_path>")
|
||||
print(" 或: freecad -c verify_stp.py <stp_file_path>")
|
||||
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)
|
||||
+19
-88
@@ -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(
|
||||
|
||||
+21
-6
@@ -115,7 +115,7 @@ class RolePermission(Base):
|
||||
return f"<RolePermission(role_id={self.role_id}, permission_id={self.permission_id})>"
|
||||
|
||||
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"<STPFile(id={self.id}, object_key='{self.object_key}', status='{self.status}')>"
|
||||
return f"<STPFile(id={self.id}, original_filename='{self.original_filename}', status='{self.status}')>"
|
||||
|
||||
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"<FeatureDetection(id={self.id}, feature_type='{self.feature_type}', confidence={self.confidence})>"
|
||||
|
||||
@@ -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"<DesignRecommendation(id={self.id}, rec_type='{self.rec_type}', priority='{self.priority}')>"
|
||||
|
||||
|
||||
@@ -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']
|
||||
batch_id = upload_batch or str(uuid.uuid4())
|
||||
|
||||
# 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
|
||||
|
||||
# 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文件及其所有关联数据"""
|
||||
|
||||
+86
-13
@@ -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 = {
|
||||
</div>
|
||||
|
||||
<div v-if="state.history?.files?.length" class="section">
|
||||
<h2 class="section-title">最近分析</h2>
|
||||
<h2 class="section-title">分析历史</h2>
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>文件名</th>
|
||||
<th>大小</th>
|
||||
<th>状态</th>
|
||||
<th>分析时间</th>
|
||||
<th>上传次数</th>
|
||||
<th>文件大小</th>
|
||||
<th>最新状态</th>
|
||||
<th>最新分析时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="file in state.history.files.slice(0, 5)" :key="file.task_id">
|
||||
<tr v-for="file in state.history.files" :key="file.filename">
|
||||
<td>{{ file.filename }}</td>
|
||||
<td>
|
||||
<span class="badge badge-info">{{ file.upload_count }} 次</span>
|
||||
</td>
|
||||
<td>{{ formatFileSize(file.file_size) }}</td>
|
||||
<td>
|
||||
<span :class="['badge', file.status === 'completed' ? 'badge-success' : file.status === 'failed' ? 'badge-error' : 'badge-warning']">
|
||||
{{ file.status }}
|
||||
<span :class="['badge', file.latest_status === 'completed' ? 'badge-success' : file.latest_status === 'failed' ? 'badge-error' : 'badge-warning']">
|
||||
{{ file.latest_status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ formatDateTime(file.created_at) }}</td>
|
||||
<td>{{ formatDateTime(file.latest_upload_time) }}</td>
|
||||
<td>
|
||||
<button v-if="file.status === 'completed'" class="btn-sm btn-primary" @click="$router.push('/moldinsight/result/' + file.task_id)">
|
||||
查看
|
||||
</button>
|
||||
<div class="action-buttons">
|
||||
<button v-if="file.latest_status === 'completed'" class="btn-sm btn-primary" @click="router.push('/moldinsight/result/' + file.latest_id)">
|
||||
查看最新
|
||||
</button>
|
||||
<button class="btn-sm btn-secondary" @click="viewFileHistory(file.filename)">
|
||||
历史记录
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="state.showFileHistory" class="modal-overlay" @click.self="state.showFileHistory = false">
|
||||
<div class="modal-content" style="max-width: 800px;">
|
||||
<div class="modal-header">
|
||||
<h2>{{ state.selectedFileHistory }} - 历史记录</h2>
|
||||
<button class="modal-close" @click="state.showFileHistory = false">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>上传时间</th>
|
||||
<th>文件大小</th>
|
||||
<th>状态</th>
|
||||
<th>体积 (mm³)</th>
|
||||
<th>表面积 (mm²)</th>
|
||||
<th>重量 (g)</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="record in state.fileHistoryRecords" :key="record.id">
|
||||
<td>{{ formatDateTime(record.upload_time) }}</td>
|
||||
<td>{{ formatFileSize(record.file_size) }}</td>
|
||||
<td>
|
||||
<span :class="['badge', record.status === 'completed' ? 'badge-success' : record.status === 'failed' ? 'badge-error' : 'badge-warning']">
|
||||
{{ record.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ record.volume ? formatNumber(record.volume) : '-' }}</td>
|
||||
<td>{{ record.surface_area ? formatNumber(record.surface_area) : '-' }}</td>
|
||||
<td>{{ record.product_weight ? record.product_weight.toFixed(2) : '-' }}</td>
|
||||
<td>
|
||||
<button v-if="record.has_analysis" class="btn-sm btn-primary" @click="viewResult(record)">
|
||||
查看详情
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user