init
This commit is contained in:
+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']
|
||||
|
||||
# 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文件及其所有关联数据"""
|
||||
|
||||
Reference in New Issue
Block a user