This commit is contained in:
2026-03-07 03:04:15 +08:00
parent bfe6da3376
commit 557fcd578a
10 changed files with 1469 additions and 201 deletions
+19 -88
View File
@@ -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
View File
@@ -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}')>"
+149 -17
View File
@@ -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文件及其所有关联数据"""