批次4后续专项完成:D11清偿 + OCC方案B实施 + 部署参数 + D2诚实标注 + CI门禁
① D11 HTML 报告 RustFS 单源化(TECH_DEBT P2 清偿):可视化产物写任务临时目录后
裸传报告键 html/reports/{filename}(文件名寻址),/html StaticFiles 挂载删除,
新增 html_report_router 根路径代理(报告键→遗留 JSON 包装→本地卷兜底→404,
防穿越);URL 形状 /html/{filename} 不变,持久化引用零迁移;celery 摘除
html_data 卷,镜像不再烤入陈旧报告;顺带删除 get_stp_file_with_data 死数据块
② OCC 方案 B(D10 清偿):run_occ(op_name, payload) 契约 + 常驻工作进程池
(occ_process_pool + occ_worker 操作注册表),超时/崩溃 terminate 换新补位、
任务级超时 recover 整体重建,残留线程泄漏根治;TopoDS 不跨进程(generate_cavity
分模 + 方案 STEP 持久化全在子进程内,返回 export_manifest);删除内存形状缓存链、
CADExporter.export_mold_results、shape_loader(→ stp_materializer)
③ OCC 方案 A 部署参数:CELERY_CONCURRENCY / CELERY_MAX_TASKS_PER_CHILD 进
Dockerfile.celery + compose + .env.example
④ D2 诚实标注:铝价响应带 source: "simulated",前端按来源渲染标注(原硬编码
"上海期货交易所"属虚假声明),死代码 getAluminumPrice 删除
⑤ CI 门禁:.gitea/workflows/ci.yml 三 job(pytest / 前端构建含 vue-tsc /
openapi 漂移检测)
接口变更三件套随批完成(openapi 76→77 paths + gen:api + 前端构建通过;方案 B
接口面零变化)。测试基线 143 passed, 0 skipped(新增 16 项)。文档六处同步。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,6 @@ import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
@@ -15,11 +13,8 @@ from typing import Optional, Dict, Any, List, Tuple
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from moldinsight.core.stp_parser import STPParser
|
||||
from moldinsight.core.geometry_analyzer import GeometryAnalyzer
|
||||
from moldinsight.core.mesh_generator import MeshGenerator
|
||||
from moldinsight.core.multi_scheme_planner import MultiSchemeMoldPlanner
|
||||
from moldinsight.core.cad_exporter import CADExporter
|
||||
from moldinsight.services.occ_process_pool import OccProcessPool
|
||||
from moldinsight.services.task_storage_service import TaskStorageService
|
||||
from moldinsight.services.analysis_storage_service import AnalysisStorageService
|
||||
from moldinsight.storage.rustfs_storage import rustfs_manager
|
||||
@@ -40,46 +35,29 @@ class ProcessingService:
|
||||
"""核心处理流程编排 — 协调 STP 解析、网格、型腔、计算、保存、验证"""
|
||||
|
||||
def __init__(self):
|
||||
self.stp_parser = STPParser()
|
||||
self.geometry_analyzer = GeometryAnalyzer()
|
||||
self.mesh_generator = MeshGenerator(quality="medium")
|
||||
self.html_generator = HTMLGenerator()
|
||||
# 批次 3 按职责拆分:任务/文件生命周期 与 分析结果数据(原 StorageIntegrationService)
|
||||
self.task_storage = TaskStorageService()
|
||||
self.analysis_storage = AnalysisStorageService()
|
||||
self.multi_scheme_planner = MultiSchemeMoldPlanner()
|
||||
# cad_exporter 仅用于导出文件条目构建/输出目录(export_artifacts 路径语义);
|
||||
# 真正的 OCC 形状导出在子进程内完成(见 core/occ_worker.py)
|
||||
self.cad_exporter = CADExporter()
|
||||
# TopoDS_Shape 为 C++ 原生内存对象,LRU 上限防止长期运行内存只涨不降
|
||||
self._export_shapes_cache: "OrderedDict[str, Dict[str, Dict[str, Any]]]" = OrderedDict()
|
||||
self._export_shapes_cache_max = 32
|
||||
# OCC 非线程安全,max_workers=1 保证所有 OCC 操作序列化执行,避免偶发崩溃
|
||||
self._occ_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="occ")
|
||||
# D11:HTMLGenerator 不再持有常驻实例,可视化产物统一写任务内临时目录后
|
||||
# 上传 RustFS 报告键(见 process_file_core)
|
||||
# OCC 方案 B:所有几何操作(解析/布尔/三角化/倒扣/转换)经常驻 OCC 进程池,
|
||||
# 进程边界干净回收超时/崩溃——替代原线程级单通道 executor(见 OCC_THROUGHPUT.md)
|
||||
self._occ_pool = OccProcessPool()
|
||||
|
||||
# ─── 对外入口 ───
|
||||
|
||||
def _reset_occ_executor(self):
|
||||
"""超时后重建 OCC executor。
|
||||
async def run_occ(self, op_name: str, payload: Dict[str, Any],
|
||||
timeout: float = 600) -> Any:
|
||||
"""在常驻 OCC 工作进程中执行同步几何操作(方案 B)。
|
||||
|
||||
asyncio.wait_for 只能取消协程,正在执行 OCC 布尔运算的线程无法中断;
|
||||
单 worker executor 中一个挂死线程会让后续任务永久排队直至重启。
|
||||
cancel_futures=True 丢弃旧 executor 中尚未开跑的排队任务(否则旧线程
|
||||
恢复后仍会继续消化旧队列,与新 executor 并发操作 OCC 必然崩溃)。
|
||||
已在运行中的 C++ 线程在 Python 层不可杀,仍会滞留——这是已知残留
|
||||
泄漏(每次超时 1 线程),根治需进程级 OCC 隔离,见 docs/OCC_THROUGHPUT.md。
|
||||
OCC 非线程安全,所有几何计算统一经本入口在独立进程中串行执行;
|
||||
超时/进程崩溃由进程池 terminate + 换新补位,干净回收(见 OCC_THROUGHPUT.md)。
|
||||
op_name 须已在 occ_worker._OPS 注册;payload 只含文件路径 + 普通字典。
|
||||
"""
|
||||
old = self._occ_executor
|
||||
self._occ_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="occ")
|
||||
old.shutdown(wait=False, cancel_futures=True)
|
||||
logger.warning("OCC executor 已因处理超时重建(排队任务已丢弃,运行中线程可能滞留 1 个)")
|
||||
|
||||
async def run_occ(self, fn, *args):
|
||||
"""在 OCC 单线程 executor 中执行同步几何操作。
|
||||
|
||||
OCC 非线程安全,所有几何计算(解析/布尔/三角化)统一经由本入口串行执行,
|
||||
避免各调用方自行创建线程池造成并发崩溃。
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(self._occ_executor, fn, *args)
|
||||
return await self._occ_pool.run(op_name, payload, timeout=timeout)
|
||||
|
||||
async def _materialize_source_file(self, stp_file: STPFile) -> Tuple[Path, Optional[Path]]:
|
||||
"""把待处理文件落到本地磁盘,返回 (本地路径, 临时目录或 None)。
|
||||
@@ -164,8 +142,8 @@ class ProcessingService:
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"处理超时: {task_id}")
|
||||
# OCC 线程无法取消:抛弃整个 executor,避免挂死线程堵死后续所有任务
|
||||
self._reset_occ_executor()
|
||||
# OCC 进程无法取消:整体重建进程池,干净杀掉可能卡死的 OCC 操作
|
||||
await self._occ_pool.recover()
|
||||
raise Exception(f"处理超时,超过{timeout_seconds}秒未完成")
|
||||
|
||||
except Exception as e:
|
||||
@@ -214,12 +192,9 @@ class ProcessingService:
|
||||
)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
loop = asyncio.get_running_loop()
|
||||
shape = await loop.run_in_executor(
|
||||
self._occ_executor, self.stp_parser.load_step_file, Path(file_path)
|
||||
)
|
||||
geometry_data = await loop.run_in_executor(
|
||||
self._occ_executor, self.stp_parser.analyze_geometry, shape
|
||||
# 方案 B:解析 + 几何分析在 OCC 子进程内一步完成(形状不跨进程)
|
||||
geometry_data = await self.run_occ(
|
||||
"parse_stp", {"stp_path": file_path}, timeout=timeout_seconds
|
||||
)
|
||||
stage_timings["parse_stp"] = round(time.perf_counter() - stage_started, 3)
|
||||
|
||||
@@ -230,7 +205,8 @@ class ProcessingService:
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
mesh_result = await self._step_generate_mesh(
|
||||
shape, geometry_data, file_path, db_session, stp_file_id, task_id
|
||||
geometry_data, file_path, db_session, stp_file_id, task_id,
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
stage_timings["generate_mesh"] = round(time.perf_counter() - stage_started, 3)
|
||||
|
||||
@@ -246,21 +222,13 @@ class ProcessingService:
|
||||
is_foam_material = MaterialService.is_foam_material(requested_material)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
plan_result = await self._step_generate_cavity(
|
||||
shape, selected_material, is_foam_material, process_params
|
||||
plan_result, export_artifacts = await self._step_generate_cavity(
|
||||
file_path, selected_material, is_foam_material, process_params,
|
||||
task_id, timeout=timeout_seconds,
|
||||
)
|
||||
stage_timings["generate_cavity"] = round(time.perf_counter() - stage_started, 3)
|
||||
export_shapes = {}
|
||||
export_artifacts = None
|
||||
if plan_result:
|
||||
export_shapes = plan_result.pop("_export_shapes", {}) or {}
|
||||
if export_shapes:
|
||||
self._cache_export_shapes(task_id, export_shapes)
|
||||
export_artifacts = self._persist_step_exports(
|
||||
task_id=task_id,
|
||||
original_filename=Path(file_path).name,
|
||||
export_shapes=export_shapes,
|
||||
)
|
||||
# 方案 B:各方案形状的持久化 STEP 已由子进程导出并返回 manifest(export_artifacts),
|
||||
# 主进程不再持有 TopoDS 引用(无跨进程传输)
|
||||
|
||||
# 4. 生成详细JSON数据 — 委托 CalculationService
|
||||
await self.task_storage.update_task_status(
|
||||
@@ -330,49 +298,60 @@ class ProcessingService:
|
||||
lod_data = mesh_result
|
||||
logger.info(f"LOD数据复用成功: {len(lods)} 级 (面数: {[lods[k]['face_count'] for k in sorted(lods.keys())]})")
|
||||
|
||||
detailed_cavity_json = await self._attach_scheme_previews(
|
||||
detailed_cavity_json=detailed_cavity_json,
|
||||
geometry_data=geometry_data,
|
||||
stp_filename=Path(file_path).name,
|
||||
pointcloud_data=pointcloud_data,
|
||||
lod_data=lod_data,
|
||||
)
|
||||
# D11:可视化产物先写任务内临时目录,再统一上传 RustFS 报告键
|
||||
# (html/reports/{filename}),不再落节点本地 html_output——
|
||||
# API 与 worker 容器文件系统不互通,本地盘从来不是可依赖的读取来源
|
||||
html_out_dir = Path(tempfile.mkdtemp(prefix="moldinsight_html_"))
|
||||
try:
|
||||
html_generator = HTMLGenerator(output_dir=str(html_out_dir))
|
||||
|
||||
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
|
||||
best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else best_cavity_data
|
||||
best_key_info = best_scheme.get("key_info", {}) if best_scheme else best_key_info
|
||||
detailed_cavity_json = await self._attach_scheme_previews(
|
||||
detailed_cavity_json=detailed_cavity_json,
|
||||
geometry_data=geometry_data,
|
||||
stp_filename=Path(file_path).name,
|
||||
pointcloud_data=pointcloud_data,
|
||||
lod_data=lod_data,
|
||||
html_generator=html_generator,
|
||||
)
|
||||
|
||||
# 8. 保存模具型腔数据(包含方案级预览链接)
|
||||
await self.analysis_storage.save_mold_cavity_data(
|
||||
db_session, stp_file_id, detailed_cavity_json
|
||||
)
|
||||
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
|
||||
best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else best_cavity_data
|
||||
best_key_info = best_scheme.get("key_info", {}) if best_scheme else best_key_info
|
||||
|
||||
html_file_path = self.html_generator.generate_and_save_visualization(
|
||||
geometry_data,
|
||||
Path(file_path).name,
|
||||
cavity_data=best_cavity_data,
|
||||
pointcloud_data=pointcloud_data,
|
||||
lod_data=lod_data,
|
||||
)
|
||||
# 8. 保存模具型腔数据(包含方案级预览链接)
|
||||
await self.analysis_storage.save_mold_cavity_data(
|
||||
db_session, stp_file_id, detailed_cavity_json
|
||||
)
|
||||
|
||||
await self.analysis_storage.save_html_file(
|
||||
db_session,
|
||||
stp_file_id,
|
||||
Path(html_file_path).name,
|
||||
html_file_path,
|
||||
)
|
||||
html_file_path = html_generator.generate_and_save_visualization(
|
||||
geometry_data,
|
||||
Path(file_path).name,
|
||||
cavity_data=best_cavity_data,
|
||||
pointcloud_data=pointcloud_data,
|
||||
lod_data=lod_data,
|
||||
)
|
||||
await self._upload_report_artifacts(Path(html_file_path))
|
||||
|
||||
await self.analysis_storage.save_html_file(
|
||||
db_session,
|
||||
stp_file_id,
|
||||
Path(html_file_path).name,
|
||||
html_file_path,
|
||||
)
|
||||
finally:
|
||||
shutil.rmtree(html_out_dir, ignore_errors=True)
|
||||
stage_timings["persist_artifacts"] = round(time.perf_counter() - stage_started, 3)
|
||||
|
||||
# 9. 分析模具设计
|
||||
stage_started = time.perf_counter()
|
||||
loop = asyncio.get_running_loop()
|
||||
analysis_result = await loop.run_in_executor(
|
||||
self._occ_executor,
|
||||
lambda: self.geometry_analyzer.analyze_mold_design(
|
||||
geometry_data,
|
||||
product_material=requested_material,
|
||||
shape=shape,
|
||||
),
|
||||
analysis_result = await self.run_occ(
|
||||
"analyze_mold_design",
|
||||
{
|
||||
"geometry_data": geometry_data,
|
||||
"product_material": requested_material,
|
||||
"stp_path": file_path,
|
||||
},
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
|
||||
if analysis_result:
|
||||
@@ -486,15 +465,15 @@ class ProcessingService:
|
||||
# ─── 内部步骤 ───
|
||||
|
||||
async def _step_generate_mesh(
|
||||
self, shape, geometry_data: dict, file_path: str,
|
||||
self, geometry_data: dict, file_path: str,
|
||||
db_session: AsyncSession, stp_file_id: int, task_id: str,
|
||||
timeout: float = 600,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""生成多级LOD网格并持久化,一次OCC剖分+trimesh简化,失败不影响主流程"""
|
||||
"""生成多级LOD网格并持久化(方案 B:子进程内一次 OCC 剖分),失败不影响主流程"""
|
||||
mesh_result = None
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
mesh_result = await loop.run_in_executor(
|
||||
self._occ_executor, self.mesh_generator.generate_multi_lod_mesh, shape
|
||||
mesh_result = await self.run_occ(
|
||||
"generate_mesh", {"stp_path": file_path}, timeout=timeout
|
||||
)
|
||||
|
||||
lod0 = mesh_result.get("lods", {}).get("0", {})
|
||||
@@ -550,97 +529,31 @@ class ProcessingService:
|
||||
return mesh_result
|
||||
|
||||
async def _step_generate_cavity(
|
||||
self, shape, selected_material: dict, is_foam_material: bool, process_params: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""生成多方案分模结果。
|
||||
self, file_path: str, selected_material: dict, is_foam_material: bool,
|
||||
process_params: Dict[str, Any], task_id: str, timeout: float = 600,
|
||||
) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]]]:
|
||||
"""生成多方案分模结果(方案 B:子进程内完成分模 + 方案形状 STEP 导出)。
|
||||
|
||||
D8:型腔是任务的核心产出,生成失败必须让任务 failed——
|
||||
此前异常在此被吞掉置 plan_result=None 继续主流程,最终任务
|
||||
completed,"完成"状态不可信。异常直接向编排层传播。
|
||||
异常直接向编排层传播。返回 (plan_result, export_manifest)。
|
||||
"""
|
||||
if not shape:
|
||||
raise RuntimeError("无有效几何 shape,无法生成模具型腔")
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
plan_result = await loop.run_in_executor(
|
||||
self._occ_executor,
|
||||
lambda: self.multi_scheme_planner.generate_plan(
|
||||
shape=shape,
|
||||
material=selected_material,
|
||||
is_foam_material=is_foam_material,
|
||||
process_params=process_params,
|
||||
),
|
||||
result = await self.run_occ(
|
||||
"generate_cavity",
|
||||
{
|
||||
"stp_path": file_path,
|
||||
"task_id": task_id,
|
||||
"material": selected_material,
|
||||
"is_foam_material": is_foam_material,
|
||||
"process_params": process_params,
|
||||
"export_out_dir": os.path.abspath(self.cad_exporter.output_dir),
|
||||
},
|
||||
timeout=timeout,
|
||||
)
|
||||
plan_result = result["plan_result"]
|
||||
logger.info(
|
||||
f"多方案分模完成: 生成 {len(plan_result.get('candidate_schemes', []))} 套方案"
|
||||
)
|
||||
return plan_result
|
||||
|
||||
def _cache_export_shapes(self, task_id: str, export_shapes: Dict[str, Dict[str, Any]]):
|
||||
self._export_shapes_cache[task_id] = export_shapes
|
||||
self._export_shapes_cache.move_to_end(task_id)
|
||||
# 逐出最旧任务的形状缓存(连原生 OCC shape 引用一起释放)
|
||||
while len(self._export_shapes_cache) > self._export_shapes_cache_max:
|
||||
self._export_shapes_cache.popitem(last=False)
|
||||
|
||||
def _persist_step_exports(
|
||||
self,
|
||||
task_id: str,
|
||||
original_filename: str,
|
||||
export_shapes: Dict[str, Dict[str, Any]],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not export_shapes:
|
||||
return None
|
||||
|
||||
base_filename = Path(original_filename).stem or f"mold_{task_id}"
|
||||
manifest = {
|
||||
"version": 1,
|
||||
"task_id": task_id,
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"schemes": {},
|
||||
}
|
||||
components = ["cavity", "core", "parting_surface", "product", "a_plate", "b_plate"]
|
||||
|
||||
for scheme_id, cavity_data in export_shapes.items():
|
||||
try:
|
||||
# 持久化装配体 STEP + 逐组件 STEP(后者是重启后按需
|
||||
# 重导出其他格式的几何来源,见 regenerate_export_from_persisted)
|
||||
result = self.cad_exporter.export_persisted_steps(
|
||||
cavity_data=cavity_data,
|
||||
base_filename=base_filename,
|
||||
components=components,
|
||||
task_id=task_id,
|
||||
scheme_id=scheme_id,
|
||||
)
|
||||
manifest["schemes"][scheme_id] = {
|
||||
"base_filename": result.get("base_filename"),
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"files": result.get("files", []),
|
||||
"errors": result.get("errors", []),
|
||||
"total_files": result.get("total_files", 0),
|
||||
"total_errors": result.get("total_errors", 0),
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.warning("持久化 STEP 导出失败: task=%s scheme=%s error=%s", task_id, scheme_id, exc)
|
||||
manifest["schemes"][scheme_id] = {
|
||||
"base_filename": base_filename,
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"files": [],
|
||||
"errors": [str(exc)],
|
||||
"total_files": 0,
|
||||
"total_errors": 1,
|
||||
}
|
||||
|
||||
return manifest
|
||||
|
||||
def get_export_shapes(self, task_id: str, scheme_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
scheme_map = self._export_shapes_cache.get(task_id)
|
||||
if not scheme_map:
|
||||
return None
|
||||
self._export_shapes_cache.move_to_end(task_id) # LRU 热点保活
|
||||
if scheme_id:
|
||||
return scheme_map.get(scheme_id)
|
||||
return next(iter(scheme_map.values()), None)
|
||||
return plan_result, result["export_manifest"]
|
||||
|
||||
async def regenerate_export_from_persisted(
|
||||
self,
|
||||
@@ -651,11 +564,12 @@ class ProcessingService:
|
||||
base_filename: str,
|
||||
scheme_files: List[Dict[str, Any]],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""内存导出缓存失效(如服务重启)后,从持久化 STEP 重建导出文件。
|
||||
"""持久化 STEP 缓存失效(如服务重启)后,从持久化 STEP 重建导出文件。
|
||||
|
||||
分析期已为每个方案持久化装配体 + 逐组件 STEP;
|
||||
缺失格式(STL/IGES/BRep)读回单组件 STEP 现场转换,
|
||||
用户无需重新分析。所有组件均不可用时返回 None。
|
||||
方案 B 后主进程不再持有 TopoDS 形状(导出持久化发生在子进程),
|
||||
内存缓存路径已随旧 _cache_export_shapes/get_export_shapes 一并删除;
|
||||
本方法读回单组件 STEP 现场转换缺失格式,用户无需重新分析。
|
||||
所有组件均不可用时返回 None。
|
||||
"""
|
||||
format_list = list(dict.fromkeys(formats or ["step", "stl"]))
|
||||
step_files = {
|
||||
@@ -693,7 +607,8 @@ class ProcessingService:
|
||||
os.path.dirname(step_path), f"{base_filename}_{comp}.{fmt}"
|
||||
)
|
||||
ok = await self.run_occ(
|
||||
self.cad_exporter.convert_component_step, step_path, out_path, fmt
|
||||
"convert_component_step",
|
||||
{"step_path": str(step_path), "out_path": str(out_path), "fmt": fmt},
|
||||
)
|
||||
if ok:
|
||||
files.append(
|
||||
@@ -763,8 +678,13 @@ class ProcessingService:
|
||||
stp_filename: str,
|
||||
pointcloud_data: Optional[Dict[str, Any]] = None,
|
||||
lod_data: Optional[Dict[str, Any]] = None,
|
||||
html_generator: HTMLGenerator = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""为候选分模方案生成轻量摘要链接(完整HTML仅最优方案按需生成)"""
|
||||
if html_generator is None:
|
||||
raise ValueError(
|
||||
"html_generator 不能为空(D11:摘要产物统一经任务临时目录上传 RustFS)"
|
||||
)
|
||||
candidate_schemes = detailed_cavity_json.get("candidate_schemes", [])
|
||||
if not candidate_schemes:
|
||||
return detailed_cavity_json
|
||||
@@ -777,10 +697,15 @@ class ProcessingService:
|
||||
base_stem = Path(stp_filename).stem.replace(" ", "_")
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
summary_name = f"mold_{base_stem}_{suffix}_{ts}_summary.json"
|
||||
summary_content = self.html_generator.generate_3d_viewer_summary(
|
||||
summary_content = html_generator.generate_3d_viewer_summary(
|
||||
geometry_data, cavity_data
|
||||
)
|
||||
self.html_generator.save_data_file(summary_content, summary_name)
|
||||
# D11:摘要 JSON 写任务临时目录后直传 RustFS 报告键,不落本地 html_output
|
||||
summary_path = html_generator.save_data_file(summary_content, summary_name)
|
||||
await rustfs_manager.upload_report_artifact(
|
||||
summary_name, Path(summary_path).read_bytes(),
|
||||
content_type="application/json",
|
||||
)
|
||||
scheme["summary_file"] = f"/html/{summary_name}"
|
||||
|
||||
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
|
||||
@@ -789,6 +714,26 @@ class ProcessingService:
|
||||
|
||||
return detailed_cavity_json
|
||||
|
||||
async def _upload_report_artifacts(self, html_file_path: Path):
|
||||
"""D11:任务临时目录中的可视化产物(.html / _summary.json / _data.json)
|
||||
上传 RustFS 报告键(html/reports/{filename})。
|
||||
|
||||
HTML 内嵌相对 DATA_URL/SUMMARY_URL 引用两个 JSON,
|
||||
三者必须同键前缀可达(读侧 /html/{filename} 直取)。
|
||||
"""
|
||||
stem = html_file_path.with_suffix("")
|
||||
artifacts = [
|
||||
(html_file_path, "text/html; charset=utf-8"),
|
||||
(stem.with_name(stem.name + "_summary.json"), "application/json"),
|
||||
(stem.with_name(stem.name + "_data.json"), "application/json"),
|
||||
]
|
||||
for path, content_type in artifacts:
|
||||
if not path.exists():
|
||||
raise RuntimeError(f"可视化产物缺失: {path.name}")
|
||||
await rustfs_manager.upload_report_artifact(
|
||||
path.name, path.read_bytes(), content_type=content_type
|
||||
)
|
||||
|
||||
# ─── 指标持久化 ───
|
||||
|
||||
async def _save_analysis_metrics(self, session: AsyncSession, stp_file_id: int, analysis_result: dict):
|
||||
|
||||
Reference in New Issue
Block a user